From f8ced42643ab3ffce680e7f5b588970567ae1340 Mon Sep 17 00:00:00 2001 From: Tsubasa SEKIGUCHI Date: Thu, 1 Oct 2026 01:05:59 +0900 Subject: [PATCH 1/5] =?UTF-8?q?trainRoute=E3=81=AB=E5=88=B0=E7=9D=80?= =?UTF-8?q?=E6=99=82=E9=96=93=E6=8E=A8=E5=AE=9A=E3=81=AE=E3=83=A2=E3=83=87?= =?UTF-8?q?=E3=83=AB=E3=81=A7=E5=8C=BA=E9=96=93=E3=81=AE=E5=80=A4=E3=82=92?= =?UTF-8?q?=E8=BF=94=E3=81=99Estimated=E3=83=A2=E3=83=BC=E3=83=89=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0=20(#1710)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Claude Opus 5.5 --- AGENTS.md | 2 +- docs/architecture.md | 19 +++ schema/public.graphql | 9 +- src/graphql/enums.rs | 21 +++ src/graphql/query.rs | 9 +- src/graphql/types.rs | 4 + src/repository.rs | 133 +++++++++++++-- stationapi/src/domain/arrival_estimation.rs | 74 +++++--- stationapi/src/model.rs | 18 ++ stationapi/src/use_case/interactor/query.rs | 176 +++++++++++++++++++- stationapi/src/use_case/traits/query.rs | 4 +- 11 files changed, 423 insertions(+), 46 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 7ab2a298..0a9ded7b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -70,7 +70,7 @@ The Worker is the workspace root package. `stationapi`, `preprocessor`, and `dat - **Stations** – `station`, `stations`, `stationGroupStations`, `stationsNearby`, `lineStations`, `stationsByName`, `lineGroupStations`, `lineListStations`, `lineGroupListStations`. `QueryInteractor` enriches stations with lines, companies, station numbers, and train types. `lineStations` resolves the line's local train-type group (rail `kind` 0/1 or a `priority > 0` type; a bullet-train line, which has no local service, takes the group of its stopping train type with the smallest `types.id`, e.g. Nozomi or Hayabusa) and returns its stops carrying that group's train type, with or without `stationId`, so a client that never picked a train type still has the `lineGroupId` `trainRoute` requires; when no such group exists — bus lines only carry `BusRoute` (`kind` 7, `priority` 0) variants — it falls back to the line's plain typeless station list so bus stop listings never return empty. `stationsByName` with `fromStationGroupId` returns the stations reachable from there: stations sharing a line group with the origin (`line_group_cd` set, `has_train_types` true), same-line stations when either side has no line group, and — for rail — stations reachable by transferring, i.e. those for which `connectedRoutes` with `viaLineId` set to the station's line returns a route (`line_group_cd` empty, `has_train_types` false). The transfer check uses `RouteTopology` (`stationapi/src/domain/route_topology.rs`), a time-free copy of the `connectedRoutes` network built straight from the index without `Station` entities or time estimates (about 20 ms instead of about 190 ms, cached in its own `OnceLock`); `RouteNetwork` holds the same topology, both share `trim_pattern` and `line_group_rows`, and a real-data test asserts the two are equal. The check is a ride-limited BFS over line groups (a few ms) plus, only for destinations that are cut vertices of the station–line-group graph, a check that arrives without stopping over at the destination group — otherwise a branch's junction station (Ishibashi-handai-mae on the Minoo Line) would be listed although reaching it on that branch means riding out and back. - **Lines** – `line`, `lines`, `linesByName`. Results include company data and computed line symbols based on repository helpers. - **Routes** – `routes`, `connectedRoutes`, `estimateArrivalTimes`, `trainRoute`. Paging tokens are currently empty (pagination not implemented). -- **`trainRoute`** – Takes the line group's stops from the repository *before* any enrichment, slices them to the requested `fromStationId`–`toStationId` range (reversing when the request runs backwards), and only then attaches lines, companies, station numbers, train types, and nearby bus routes. Enrichment is per-station and independent, so slicing first does not change any segment; enriching the whole line group first made a three-station request cost the same as a 250-station one. Keep the order — the cost of this query must stay proportional to the requested range, not to the line group. +- **`trainRoute`** – Takes the line group's stops from the repository *before* any enrichment, slices them to the requested `fromStationId`–`toStationId` range (reversing when the request runs backwards), and only then attaches lines, companies, station numbers, train types, and nearby bus routes. Enrichment is per-station and independent, so slicing first does not change any segment; enriching the whole line group first made a three-station request cost the same as a 250-station one. Keep the order — the cost of this query must stay proportional to the requested range, not to the line group. The optional `model` argument (`TrainRouteModel`) picks how the segment values are computed. `Legacy`, the default, keeps the values from when the query was added (#1568): `dto::simulation::resolve_speed_profile` supplies the speed and acceleration, and `arrivalCumulativeMinutes` / `departureCumulativeMinutes` stay `null`. TrainLCD/MobileApp's auto mode depends on these values, so do not change them. `Estimated` runs `arrival_estimation` — the model the speed tables are calibrated against — on the same pre-enrichment slice (with `legs`, it reuses `estimate_connected_route_arrival_times`, so the values equal `estimateArrivalTimes` given the same `legs`). It then replaces each segment's `stops`, speed, and acceleration with the estimator's and fills the two cumulative fields. A route containing a bus station keeps the `Legacy` values, because the estimator's kinematics do not apply to buses. - **Coordinate lookups** – `index::nearest` (k nearest, used by `stationsNearby`) and `index::within_radius` (everything inside a radius, used by the nearby-bus-stop enrichment) both go through a per-transport-type grid index (`Grid`, CSR over 0.05° cells) instead of scanning the whole station table. `nearest` searches a radius, widens it while fewer than `limit` stations fall inside, and stops once the radius covers the index — anything outside a radius that already holds `limit` hits cannot be in the top `limit`. With `transportType` omitted it returns rail stations first and bus stops after, each group sorted by distance — the pre-Workers SQL's `ORDER BY transport_type, distance`. The limit applies to the merged order, so `nearest` fills it with rail and only asks the bus grid for the remaining slots; a location with `limit` rail stations returns no bus stops at all. Ties on distance break on `station_cd` so the order does not depend on an unstable sort. Every station lookup by coordinates runs on every request that enriches rail stations with nearby bus routes, so keep new coordinate queries on the grid rather than adding another full scan. - **Train types** – `stationTrainTypes`, `routeTypes`. Train types aggregate by line group and include related lines plus optional train type metadata. Rail variants use `TrainTypeKind::{Default, Branch, Rapid, Express, LimitedExpress, HighSpeedRapid, CommuterRapid}` (0-6); bus variants use `BusRoute` (7), which represents a `(route_id, shape_id)` operation pattern (e.g. 循環 / 短ターン / 支線) generated automatically from the configured GTFS bus feeds (Toei Bus, Seibu Bus, Keio Bus) and the converted Tokyu Bus JSON. - **Default rail train types** – `preprocessor` fills every active rail line containing at least one station with no `station_station_types` row with a deterministic, complete all-stop group. The generated rows exist only in `generated/*.csv`; canonical CSV files remain unchanged. `type_cd=100` represents 「普通」 and `type_cd=101` represents 「各駅停車」. An existing 100/101 assignment on the line takes precedence; otherwise the label is selected per line through `LOCAL_SERVICE_RAIL_LINE_IDS` in `preprocessor/src/rail.rs`. Generated `line_group_cd` values use `1,000,000,000 + line_cd`; generation fails on a collision. Bus lines are excluded and continue to use their GTFS-derived `BusRoute` groups. diff --git a/docs/architecture.md b/docs/architecture.md index 73afaaa2..3629a332 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -475,6 +475,25 @@ input RouteLegInput { lineGroupId: Int! fromStationId: Int! toStationId: Int! なので、各区間の最初の駅の `distanceFromPrevious` は 0 になり、通過駅が あるか (優等種別の速度を使うか) も区間ごとに判定します。 +`trainRoute` は、区間の値をどのモデルで出すかを `model: TrainRouteModel` で +選べます。 + +- `Legacy` (省略時): 追加した時点 (#1568) のモデルです。最高速度と加減速は + `dto::simulation::resolve_speed_profile` が決め、到着・出発の見込み + (`arrivalCumulativeMinutes` / `departureCumulativeMinutes`) は `null` です。 + MobileApp のオートモードがこの値で走るので、値を変えません。 +- `Estimated`: 到着時間推定 (`arrival_estimation`) のモデルです。速度の較正 + テーブルは、このモデルに当てはめて求めています。返す駅列に推定を掛け、停車・ + 通過、最高速度、加減速を推定が使った値に置き換えて、到着・出発の見込みを入れ + ます。`legs` を渡したときの見込みは、同じ `legs` を渡した + `estimateArrivalTimes` と同じ値です。バスの駅を含む経路は推定のモデルの対象外 + なので、`Legacy` と同じ値を返します。 + +2 つのモデルは、加減速、運転余裕率、停車時間、較正テーブルに無い路線・種別の +速度が違います。`Legacy` の値で台形の速度プロファイルを作って走らせると、 +`estimateArrivalTimes` より短い時間で走り切ります (#1709)。所要時間を推定に +合わせたいクライアントは、`Estimated` の見込みを使います。 + 区間の切り出しには両者で同じ関数を使い、環状線では継ぎ目をまたぐ短いほうの 弧を選ぶので、両者の駅の並びは一致します (`lineGroupId` を指定した `trainRoute` は、従来どおり格納順で切り出します)。次のいずれかに当てはまる diff --git a/schema/public.graphql b/schema/public.graphql index 8907141e..e84628a8 100644 --- a/schema/public.graphql +++ b/schema/public.graphql @@ -66,6 +66,11 @@ enum ConnectedRouteSort { TransferCount } +enum TrainRouteModel { + Legacy + Estimated +} + enum TtsAlphabet { TtsAlphabetUnspecified Ipa @@ -100,7 +105,7 @@ type Query { routeTypes(fromStationGroupId: Int!, toStationGroupId: Int!, viaLineId: Int, pageSize: Int, pageToken: String): RouteTypePage! connectedRoutes(fromStationGroupId: Int!, toStationGroupId: Int!, viaLineId: Int, sortBy: ConnectedRouteSort): [ConnectedRoute!]! estimateArrivalTimes(fromStationId: Int!, toStationId: Int!, viaLineIds: [Int!], directionId: Int, legs: [RouteLegInput!]): EstimatedArrivalPage! - trainRoute(fromStationId: Int!, toStationId: Int!, lineGroupId: Int, legs: [RouteLegInput!]): TrainRouteResponse! + trainRoute(fromStationId: Int!, toStationId: Int!, lineGroupId: Int, legs: [RouteLegInput!], model: TrainRouteModel): TrainRouteResponse! } type ConnectedRoute { @@ -330,6 +335,8 @@ type TrainRouteSegment { maxSpeed: Float maxAcceleration: Float maxDeceleration: Float + arrivalCumulativeMinutes: Float + departureCumulativeMinutes: Float } type TrainRouteResponse { diff --git a/src/graphql/enums.rs b/src/graphql/enums.rs index f0df47de..54c85515 100644 --- a/src/graphql/enums.rs +++ b/src/graphql/enums.rs @@ -7,6 +7,7 @@ use async_graphql::Enum; use stationapi::domain::route_search::JourneySort; +use stationapi::model; #[derive(Enum, Copy, Clone, Eq, PartialEq)] #[graphql(rename_items = "PascalCase", name = "LineType")] @@ -111,6 +112,26 @@ impl From for JourneySort { } } +// trainRoute が区間の値をどのモデルで出すか +#[derive(Enum, Copy, Clone, Eq, PartialEq)] +#[graphql(rename_items = "PascalCase", name = "TrainRouteModel")] +pub enum TrainRouteModel { + // 追加した時点のモデル。到着・出発の見込みは返さない (null)。既定 + Legacy, + // estimateArrivalTimes と同じ到着時間推定のモデル。到着・出発の見込みと、 + // 推定が使った最高速度・加減速を返す。バスの駅を含む経路は Legacy と同じ値 + Estimated, +} + +impl From for model::TrainRouteModel { + fn from(value: TrainRouteModel) -> Self { + match value { + TrainRouteModel::Legacy => model::TrainRouteModel::Legacy, + TrainRouteModel::Estimated => model::TrainRouteModel::Estimated, + } + } +} + macro_rules! from_i32 { ($ty:ident, $default:expr, $($value:expr => $variant:ident),+ $(,)?) => { impl From for $ty { diff --git a/src/graphql/query.rs b/src/graphql/query.rs index 17a55750..5894d6c4 100644 --- a/src/graphql/query.rs +++ b/src/graphql/query.rs @@ -10,7 +10,7 @@ use stationapi::domain::route_search; use stationapi::model; use stationapi::use_case::traits::query::QueryUseCase; -use super::enums::{ConnectedRouteSort, TransportType as GqlTransportType}; +use super::enums::{ConnectedRouteSort, TrainRouteModel, TransportType as GqlTransportType}; use super::scalar::UInt32; use super::types::*; use crate::Interactor; @@ -473,7 +473,9 @@ impl QueryRoot { to_station_id: i32, line_group_id: Option, legs: Option>, + model: Option, ) -> GqlResult { + let route_model = model.map(Into::into).unwrap_or_default(); // 乗換経路: 区間ごとの走行区間を順につなげる。系統は区間ごとに決まるので // lineGroupId とは併用しない if let Some(legs) = legs { @@ -487,7 +489,9 @@ impl QueryRoot { to_id(from_station_id, "fromStationId")?, to_id(to_station_id, "toStationId")?, )?; - let segments = use_case(ctx).get_connected_train_route(&legs).await?; + let segments = use_case(ctx) + .get_connected_train_route(&legs, route_model) + .await?; return Ok(TrainRouteResponse { segments: Some(segments.into_iter().map(Into::into).collect()), }); @@ -497,6 +501,7 @@ impl QueryRoot { to_id(from_station_id, "fromStationId")?, to_id(to_station_id, "toStationId")?, to_opt_id(line_group_id, "lineGroupId")?, + route_model, ) .await?; Ok(TrainRouteResponse { diff --git a/src/graphql/types.rs b/src/graphql/types.rs index fd4589df..8c164d53 100644 --- a/src/graphql/types.rs +++ b/src/graphql/types.rs @@ -418,6 +418,8 @@ pub struct TrainRouteSegment { pub max_speed: Option, pub max_acceleration: Option, pub max_deceleration: Option, + pub arrival_cumulative_minutes: Option, + pub departure_cumulative_minutes: Option, } impl From for TrainRouteSegment { @@ -429,6 +431,8 @@ impl From for TrainRouteSegment { max_speed: Some(v.max_speed), max_acceleration: Some(v.max_acceleration), max_deceleration: Some(v.max_deceleration), + arrival_cumulative_minutes: v.arrival_cumulative_minutes, + departure_cumulative_minutes: v.departure_cumulative_minutes, } } } diff --git a/src/repository.rs b/src/repository.rs index 1ebbbf97..945c8bf9 100644 --- a/src/repository.rs +++ b/src/repository.rs @@ -1249,7 +1249,7 @@ impl TrainTypeRepository for MemTrainTypeRepository { mod tests { use super::*; use stationapi::domain::route_search::{self, Journey, JourneySort}; - use stationapi::model; + use stationapi::model::{self, TrainRouteModel}; const TOKYO: u32 = 1130101; const SHIBUYA: u32 = 1130205; @@ -1304,7 +1304,9 @@ mod tests { let legs = journey_legs(journey); let eta = block_on(interactor.estimate_connected_route_arrival_times(&legs)).unwrap(); - let train_route = block_on(interactor.get_connected_train_route(&legs)).unwrap(); + let train_route = + block_on(interactor.get_connected_train_route(&legs, TrainRouteModel::Legacy)) + .unwrap(); // 同じ区間を同じ弧で切り出す let eta_ids = station_ids_of(&eta); @@ -1364,7 +1366,8 @@ mod tests { first != 1131220 && last != 1131211, "the local's own stations" ); - let train_route = block_on(interactor.get_connected_train_route(&legs)).unwrap(); + let train_route = + block_on(interactor.get_connected_train_route(&legs, TrainRouteModel::Legacy)).unwrap(); assert_eq!(train_route.len(), eta.len()); // connectedRoutes の区間の trainTypes は、どれを選んでも区間の乗降駅で使える @@ -1409,7 +1412,9 @@ mod tests { .collect(); let eta = block_on(interactor.estimate_connected_route_arrival_times(&legs)).unwrap(); - let train_route = block_on(interactor.get_connected_train_route(&legs)).unwrap(); + let train_route = + block_on(interactor.get_connected_train_route(&legs, TrainRouteModel::Legacy)) + .unwrap(); assert_eq!(eta.len(), train_route.len()); } } @@ -1480,8 +1485,12 @@ mod tests { .unwrap_err() .to_string(); assert!(error.contains("区間がつながっていません"), "{error}"); - assert!(block_on(interactor.get_connected_train_route(&legs)).is_err()); - assert!(block_on(interactor.get_connected_train_route(&[])).is_err()); + assert!( + block_on(interactor.get_connected_train_route(&legs, TrainRouteModel::Legacy)).is_err() + ); + assert!( + block_on(interactor.get_connected_train_route(&[], TrainRouteModel::Legacy)).is_err() + ); // connectedRoutes が返しうる乗車回数 (MAX_RIDES) を超える区間は断る。 // つながった区間 (三鷹と新宿を中央線快速で往復) でも受け付けない @@ -1503,10 +1512,16 @@ mod tests { .unwrap_err() .to_string(); assert!(error.contains("区間までにしてください"), "{error}"); - assert!(block_on(interactor.get_connected_train_route(&back_and_forth)).is_err()); + assert!(block_on( + interactor.get_connected_train_route(&back_and_forth, TrainRouteModel::Legacy) + ) + .is_err()); // 上限ちょうどは受け付ける let at_limit = &back_and_forth[..route_search::MAX_RIDES]; - assert!(block_on(interactor.get_connected_train_route(at_limit)).is_ok()); + assert!( + block_on(interactor.get_connected_train_route(at_limit, TrainRouteModel::Legacy)) + .is_ok() + ); } #[test] @@ -1637,13 +1652,109 @@ mod tests { .expect("駅に種別が付いていない") as u32; let ids: Vec = stations.iter().map(|s| s.station_cd as u32).collect(); - let segments = - block_on(interactor.get_train_route(ids[0], *ids.last().unwrap(), Some(line_group_id))) - .unwrap(); + let segments = block_on(interactor.get_train_route( + ids[0], + *ids.last().unwrap(), + Some(line_group_id), + TrainRouteModel::Legacy, + )) + .unwrap(); let route_ids: Vec = segments .iter() .filter_map(|s| s.station.as_ref().map(|st| st.id)) .collect(); assert_eq!(route_ids, ids); } + + /// Estimated の trainRoute は、同じ legs を渡した estimateArrivalTimes と同じ + /// 見込みを返し、停車・加減速も推定のモデルにそろう。Legacy は見込みを返さず、 + /// 区間の値も変わらない。 + #[test] + fn estimated_connected_train_route_carries_the_estimate_arrival_times() { + use stationapi::use_case::traits::query::QueryUseCase; + let interactor = crate::interactor(); + let params = EstimationParams::default(); + for (from, to) in [(1131906, 1160213), (TOKYO, SHIBUYA)] { + for journey in &route_network().search(from, to, None) { + let legs = journey_legs(journey); + let eta = + block_on(interactor.estimate_connected_route_arrival_times(&legs)).unwrap(); + let legacy = + block_on(interactor.get_connected_train_route(&legs, TrainRouteModel::Legacy)) + .unwrap(); + let estimated = block_on( + interactor.get_connected_train_route(&legs, TrainRouteModel::Estimated), + ) + .unwrap(); + + assert_eq!(estimated.len(), eta.len()); + for (i, (segment, stop)) in estimated.iter().zip(&eta).enumerate() { + assert_eq!( + segment.arrival_cumulative_minutes, + Some(stop.cumulative_minutes) + ); + assert_eq!( + segment.departure_cumulative_minutes, + Some(stop.departure_cumulative_minutes) + ); + assert_eq!(segment.stops, stop.stops_here); + assert_eq!(segment.max_speed, stop.max_speed_kmh / 3.6); + assert_eq!(segment.max_acceleration, params.accel); + assert_eq!(segment.max_deceleration, params.decel); + // 駅と距離は Legacy と同じ + assert_eq!(segment.station, legacy[i].station); + assert_eq!( + segment.distance_from_previous, + legacy[i].distance_from_previous + ); + } + assert!(legacy.iter().all(|segment| { + segment.arrival_cumulative_minutes.is_none() + && segment.departure_cumulative_minutes.is_none() + })); + } + } + } + + /// lineGroupId で呼んだ Estimated の trainRoute は、その区間を 1 つの leg にした + /// estimateArrivalTimes と同じ見込みになる (環状線でない系統では同じ駅列を + /// 同じ較正母数で推定するため)。#1709 の 2 経路で確かめる。 + #[test] + fn estimated_train_route_matches_a_single_leg_estimate() { + use stationapi::use_case::traits::query::QueryUseCase; + let interactor = crate::interactor(); + // 東急東横線 特急 渋谷→横浜、京王線 特急 新宿→京王八王子 + for (line_group_id, from, to) in [(161, 2600101, 2600121), (71, 2400101, 2400134)] { + let segments = block_on(interactor.get_train_route( + from, + to, + Some(line_group_id), + TrainRouteModel::Estimated, + )) + .unwrap(); + let eta = block_on(interactor.estimate_connected_route_arrival_times(&[ + model::RouteLegRequest { + line_group_id, + from_station_id: from, + to_station_id: to, + }, + ])) + .unwrap(); + assert_eq!(segments.len(), eta.len()); + for (segment, stop) in segments.iter().zip(&eta) { + assert_eq!( + segment.station.as_ref().map(|station| station.id as i32), + Some(stop.station_cd) + ); + assert_eq!( + segment.arrival_cumulative_minutes, + Some(stop.cumulative_minutes) + ); + assert_eq!( + segment.departure_cumulative_minutes, + Some(stop.departure_cumulative_minutes) + ); + } + } + } } diff --git a/stationapi/src/domain/arrival_estimation.rs b/stationapi/src/domain/arrival_estimation.rs index 60284185..258cb2a5 100644 --- a/stationapi/src/domain/arrival_estimation.rs +++ b/stationapi/src/domain/arrival_estimation.rs @@ -66,6 +66,10 @@ pub struct EstimatedStop { pub departure_cumulative_minutes: f64, /// その駅に停車するか(false = 通過)。 pub stops_here: bool, + /// 直前の駅からこの駅までを走るときの最高速度(km/h)。推定が走行時間の計算に + /// 使った値そのもの。始点は、始点の駅に対して同じ規則で決めた速度 + /// (直前の駅が無いので駅間別の較正は掛からない)。 + pub max_speed_kmh: f64, } /// 推定で使う調整可能なパラメータ。すべて「実距離・実速度・ダイヤが無い」前提の @@ -615,26 +619,8 @@ pub fn estimate_arrival_minutes_calibrated( let line_group_of = |station: &Station| -> Option { station.line_group_cd.or(Some(station.line_cd)) }; - let mut result: Vec = Vec::with_capacity(n); - - // 始点。即時出発とみなすので到着・出発とも 0 分。 - result.push(EstimatedStop { - station_cd: stops[0].station_cd, - station_g_cd: stops[0].station_g_cd, - line_group_cd: line_group_of(stops[0]), - cumulative_minutes: 0.0, - departure_cumulative_minutes: 0.0, - stops_here: stops_here[0], - }); - - // 直前の停車駅を出発した時刻(分)。始点は即時出発なので 0。 - let mut last_departure = 0.0_f64; - // 現在の停車間セグメントに溜めるサブ区間。 - // (みなし走行距離 m, 最高速度 km/h, result index, 停車駅か) - let mut seg: Vec<(f64, f64, usize, bool)> = Vec::new(); - - for i in 1..n { - let track_m = straight_km[i] * detour_of(stops[i]) * 1000.0; + // 直前の駅から i 番目の駅までを走るときの最高速度(km/h)。 + let speed_kmh_at = |i: usize| -> f64 { // この路線に通過駅が無ければ各駅停車として振る舞う(種別倍率なし・ // Default の速度較正を使用)。 let effective_kind = if line_has_pass @@ -655,7 +641,8 @@ pub fn estimate_arrival_minutes_calibrated( // 隣接駅ペア単位の較正(GTFS 実ダイヤ由来)があれば路線単位の速度より // 優先する。急曲線・急勾配で路線平均より遅い区間(大江戸線 月島〜赤羽橋 // など)の区間差を反映する。各停系種別の鉄道のみ。 - if stops[i].transport_type != TransportType::Bus + if i > 0 + && stops[i].transport_type != TransportType::Bus && segment_override_applies_to_kind(effective_kind) { if let Some(v) = segment_speed_override_kmh( @@ -666,6 +653,31 @@ pub fn estimate_arrival_minutes_calibrated( v_kmh = v; } } + v_kmh + }; + + let mut result: Vec = Vec::with_capacity(n); + + // 始点。即時出発とみなすので到着・出発とも 0 分。 + result.push(EstimatedStop { + station_cd: stops[0].station_cd, + station_g_cd: stops[0].station_g_cd, + line_group_cd: line_group_of(stops[0]), + cumulative_minutes: 0.0, + departure_cumulative_minutes: 0.0, + stops_here: stops_here[0], + max_speed_kmh: speed_kmh_at(0), + }); + + // 直前の停車駅を出発した時刻(分)。始点は即時出発なので 0。 + let mut last_departure = 0.0_f64; + // 現在の停車間セグメントに溜めるサブ区間。 + // (みなし走行距離 m, 最高速度 km/h, result index, 停車駅か) + let mut seg: Vec<(f64, f64, usize, bool)> = Vec::new(); + + for i in 1..n { + let track_m = straight_km[i] * detour_of(stops[i]) * 1000.0; + let v_kmh = speed_kmh_at(i); let idx = result.len(); result.push(EstimatedStop { @@ -675,6 +687,7 @@ pub fn estimate_arrival_minutes_calibrated( cumulative_minutes: 0.0, departure_cumulative_minutes: 0.0, stops_here: stops_here[i], + max_speed_kmh: v_kmh, }); seg.push((track_m, v_kmh, idx, stops_here[i])); @@ -1641,4 +1654,23 @@ mod tests { let expected = segment_run_minutes(straight_m * 1.06, 80.0, &p); approx(est[1].cumulative_minutes, expected); } + + #[test] + fn estimated_stops_carry_the_speed_used_for_the_run() { + let p = EstimationParams::default(); + // 在来線・種別なしは 80km/h。2 駅だけなら走行時間はその速度の台形そのもの。 + // みなし走行距離は直線 × 1.06 (西武池袋線の迂回係数の較正値)。 + let a = station(1, 22001, 35.743563, 139.606981, Some(2041.41825)); + let b = station(2, 22001, 35.749406, 139.586732, Some(2041.41825)); + let stations = [a, b]; + let refs: Vec<&Station> = stations.iter().collect(); + let est = estimate_arrival_minutes(&refs, &p); + approx(est[0].max_speed_kmh, 80.0); + approx(est[1].max_speed_kmh, 80.0); + let track_m = haversine_distance(35.743563, 139.606981, 35.749406, 139.586732) * 1.06; + approx( + est[1].cumulative_minutes, + segment_run_minutes(track_m, est[1].max_speed_kmh, &p), + ); + } } diff --git a/stationapi/src/model.rs b/stationapi/src/model.rs index 94eb757b..e876e9c5 100644 --- a/stationapi/src/model.rs +++ b/stationapi/src/model.rs @@ -343,6 +343,19 @@ pub struct RouteLeg { pub station_group_ids: Vec, } +/// `trainRoute` がどのモデルで区間の値を出すか。 +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum TrainRouteModel { + /// 追加した時点 (#1568) のモデル。`dto::simulation::resolve_speed_profile` の + /// 最高速度・加減速を返し、到着・出発の見込みは返さない。MobileApp の + /// オートモードが使う。既定。 + #[default] + Legacy, + /// 到着時間推定 (`arrival_estimation`) のモデル。`estimateArrivalTimes` と同じ + /// 到着・出発の見込みと、推定が使った最高速度・加減速を返す。 + Estimated, +} + /// 走行シミュレーション用の 1 区間。 #[derive(Clone, Debug, Default, PartialEq)] pub struct TrainRouteSegment { @@ -358,6 +371,11 @@ pub struct TrainRouteSegment { pub max_acceleration: f64, /// 最大減速度 (m/s^2)。正値 (絶対値として持つ)。 pub max_deceleration: f64, + /// 始点からの累積到着時刻 (分)。`TrainRouteModel::Estimated` のときだけ入る。 + pub arrival_cumulative_minutes: Option, + /// 始点からの累積出発時刻 (分)。途中の停車駅では到着に停車時間を足した値、 + /// 通過駅と終点では到着と同じ。`TrainRouteModel::Estimated` のときだけ入る。 + pub departure_cumulative_minutes: Option, } #[cfg(test)] diff --git a/stationapi/src/use_case/interactor/query.rs b/stationapi/src/use_case/interactor/query.rs index 85c78bdd..d8f6d041 100644 --- a/stationapi/src/use_case/interactor/query.rs +++ b/stationapi/src/use_case/interactor/query.rs @@ -889,6 +889,7 @@ where from_station_id: u32, to_station_id: u32, line_group_id: Option, + route_model: model::TrainRouteModel, ) -> Result, UseCaseError> { let line_group_id = line_group_id.ok_or_else(|| UseCaseError::NotFound { entity_type: "line group", @@ -926,7 +927,16 @@ where v.reverse(); v }; - self.train_route_segments(sliced, line_group_id).await + // 推定は付帯情報を付ける前の駅列で行う (estimateArrivalTimes と同じ入力) + let estimated = match route_model { + model::TrainRouteModel::Legacy => None, + model::TrainRouteModel::Estimated => estimate_train_route_slice(&sliced, &stations), + }; + let mut segments = self.train_route_segments(sliced, line_group_id).await?; + if let Some(estimated) = estimated { + apply_estimated_model(&mut segments, &estimated)?; + } + Ok(segments) } async fn estimate_connected_route_arrival_times( @@ -990,9 +1000,12 @@ where async fn get_connected_train_route( &self, legs: &[model::RouteLegRequest], + route_model: model::TrainRouteModel, ) -> Result, UseCaseError> { let group_of = self.validate_route_legs(legs).await?; + let estimated_model = route_model == model::TrainRouteModel::Estimated; let mut segments = Vec::new(); + let mut has_bus = false; for leg in legs { let stations = self .station_repository @@ -1012,9 +1025,16 @@ where }, ) .ok_or_else(|| leg_not_found(leg))?; + has_bus = has_bus || (estimated_model && sliced.iter().any(is_bus_station)); // 区間ごとに別の列車なので、通過駅の有無や距離の起点も区間ごとに数える segments.extend(self.train_route_segments(sliced, leg.line_group_id).await?); } + // 見込みは estimateArrivalTimes に同じ legs を渡したときと同じ値にする + // (乗換の徒歩と待ち時間の見込みも含む)。バスは推定のモデルの対象外 + if estimated_model && !has_bus { + let estimated = self.estimate_connected_route_arrival_times(legs).await?; + apply_estimated_model(&mut segments, &estimated)?; + } Ok(segments) } @@ -1348,6 +1368,8 @@ where max_speed: profile.max_speed, max_acceleration: profile.max_acceleration, max_deceleration: profile.max_deceleration, + arrival_cumulative_minutes: None, + departure_cumulative_minutes: None, }); } @@ -2004,6 +2026,68 @@ fn slice_group_stations( }) } +fn is_bus_station(station: &Station) -> bool { + station.transport_type == TransportType::Bus +} + +/// `trainRoute` (lineGroupId 指定) が返す駅列 `sliced` の到着見込み。 +/// +/// 迂回係数の較正母数には、切り出す前の系統全体 `group` を渡す +/// (`estimate_group_segment` と同じく、閉じた環状データの重複終端は除く)。 +/// バスの駅を含むときは推定のモデルの対象外として `None` を返す。 +fn estimate_train_route_slice(sliced: &[Station], group: &[Station]) -> Option> { + if sliced.iter().any(is_bus_station) { + return None; + } + let stops: Vec<&Station> = sliced.iter().collect(); + let mut route: Vec<&Station> = group.iter().collect(); + if route.len() > 1 && route[0].station_cd == route[route.len() - 1].station_cd { + route.pop(); + } + Some(estimate_arrival_minutes_calibrated( + &stops, + &route, + &EstimationParams::default(), + )) +} + +/// `trainRoute` の区間の値を、到着見込み `estimated` のモデルの値に置き換える +/// (`TrainRouteModel::Estimated`)。 +/// +/// 停車・通過、最高速度、加減速、到着・出発の見込みを、すべて推定が使った値に +/// そろえる。停車の判定もそろえるのは、クライアントが停車駅で区切った区間と、 +/// 見込みの付いた停車駅がずれないようにするため。`estimated` は `segments` と +/// 同じ駅列に対するもので、並びがずれていれば別の駅の値を返すことになるので +/// エラーにする。 +fn apply_estimated_model( + segments: &mut [model::TrainRouteSegment], + estimated: &[EstimatedStop], +) -> Result<(), UseCaseError> { + let aligned = segments.len() == estimated.len() + && segments.iter().zip(estimated).all(|(segment, stop)| { + segment + .station + .as_ref() + .map(|station| i64::from(station.id)) + == Some(i64::from(stop.station_cd)) + }); + if !aligned { + return Err(UseCaseError::Unexpected( + "trainRoute の駅列と到着見込みの駅列が一致しません".to_string(), + )); + } + let params = EstimationParams::default(); + for (segment, stop) in segments.iter_mut().zip(estimated) { + segment.stops = stop.stops_here; + segment.max_speed = stop.max_speed_kmh / 3.6; + segment.max_acceleration = params.accel; + segment.max_deceleration = params.decel; + segment.arrival_cumulative_minutes = Some(stop.cumulative_minutes); + segment.departure_cumulative_minutes = Some(stop.departure_cumulative_minutes); + } + Ok(()) +} + /// 区間の乗降駅が指定された系統に見つからないときのエラー。 fn leg_not_found(leg: &model::RouteLegRequest) -> UseCaseError { UseCaseError::NotFound { @@ -6198,7 +6282,7 @@ mod tests { let (interactor, _) = build_interactor(build_line_group(20)); let segments = interactor - .get_train_route(1004, 1002, Some(1000)) + .get_train_route(1004, 1002, Some(1000), model::TrainRouteModel::Legacy) .await .unwrap(); @@ -6214,7 +6298,7 @@ mod tests { let (interactor, calls) = build_interactor(build_line_group(20)); let segments = interactor - .get_train_route(1002, 1004, Some(1000)) + .get_train_route(1002, 1004, Some(1000), model::TrainRouteModel::Legacy) .await .unwrap(); @@ -6232,7 +6316,7 @@ mod tests { let (interactor, calls) = build_interactor(build_line_group(20)); let segments = interactor - .get_train_route(1004, 1002, Some(1000)) + .get_train_route(1004, 1002, Some(1000), model::TrainRouteModel::Legacy) .await .unwrap(); @@ -6254,7 +6338,7 @@ mod tests { let (interactor, _) = build_interactor(build_line_group(20)); let segments = interactor - .get_train_route(1002, 1006, Some(1000)) + .get_train_route(1002, 1006, Some(1000), model::TrainRouteModel::Legacy) .await .unwrap(); @@ -6277,7 +6361,7 @@ mod tests { } let (local_interactor, _) = build_interactor(all_stops); let local_segments = local_interactor - .get_train_route(1002, 1006, Some(1000)) + .get_train_route(1002, 1006, Some(1000), model::TrainRouteModel::Legacy) .await .unwrap(); @@ -6295,6 +6379,80 @@ mod tests { ); } + /// Legacy は到着・出発の見込みを返さない。区間の値は追加前と同じ規則 + /// (resolve_speed_profile) のまま。 + #[tokio::test] + async fn legacy_model_returns_no_estimate() { + let (interactor, _) = build_interactor(build_line_group(20)); + let segments = interactor + .get_train_route(1002, 1006, Some(1000), model::TrainRouteModel::Legacy) + .await + .unwrap(); + assert!(segments.iter().all(|s| { + s.arrival_cumulative_minutes.is_none() && s.departure_cumulative_minutes.is_none() + })); + // 加減速は追加時点 (#1568) の在来線の値 + assert!(segments + .iter() + .all(|s| s.max_acceleration == 0.83 && s.max_deceleration == 0.69)); + } + + /// Estimated は、同じ駅列を系統全体で較正した到着時間推定の値を返す。 + #[tokio::test] + async fn estimated_model_follows_the_arrival_estimation() { + let group = build_line_group(20); + let (interactor, _) = build_interactor(group.clone()); + let segments = interactor + .get_train_route(1002, 1006, Some(1000), model::TrainRouteModel::Estimated) + .await + .unwrap(); + + let slice: Vec<&Station> = group[2..=6].iter().collect(); + let whole: Vec<&Station> = group.iter().collect(); + let params = EstimationParams::default(); + let expected = estimate_arrival_minutes_calibrated(&slice, &whole, ¶ms); + + assert_eq!(segments.len(), expected.len()); + for (segment, stop) in segments.iter().zip(&expected) { + assert_eq!(segment.stops, stop.stops_here); + assert_eq!( + segment.arrival_cumulative_minutes, + Some(stop.cumulative_minutes) + ); + assert_eq!( + segment.departure_cumulative_minutes, + Some(stop.departure_cumulative_minutes) + ); + assert_eq!(segment.max_speed, stop.max_speed_kmh / 3.6); + assert_eq!(segment.max_acceleration, params.accel); + assert_eq!(segment.max_deceleration, params.decel); + } + // 通過駅があり、終点の到着は 0 より後 + assert!(segments.iter().any(|s| !s.stops)); + assert!(segments.last().unwrap().arrival_cumulative_minutes.unwrap() > 0.0); + } + + /// バスの駅を含む経路は推定のモデルの対象外なので、Estimated でも + /// Legacy と同じ値を返す。 + #[tokio::test] + async fn estimated_model_leaves_bus_routes_as_legacy() { + let mut group = build_line_group(8); + for station in group.iter_mut() { + station.transport_type = TransportType::Bus; + } + let (interactor, _) = build_interactor(group.clone()); + let legacy = interactor + .get_train_route(1001, 1005, Some(1000), model::TrainRouteModel::Legacy) + .await + .unwrap(); + let (interactor, _) = build_interactor(group); + let estimated = interactor + .get_train_route(1001, 1005, Some(1000), model::TrainRouteModel::Estimated) + .await + .unwrap(); + assert_eq!(estimated, legacy); + } + /// 近傍バス停の探索半径には、駅グループの代表座標と各駅の座標の隔たりを /// 足す。足さないと、代表からは 300m を超えるが同じグループの別の駅からは /// 300m 以内、というバス停を取りこぼす。 @@ -6315,7 +6473,7 @@ mod tests { let (interactor, calls) = build_interactor(stations); interactor - .get_train_route(1000, 1003, Some(1000)) + .get_train_route(1000, 1003, Some(1000), model::TrainRouteModel::Legacy) .await .unwrap(); @@ -6333,7 +6491,7 @@ mod tests { let (interactor, _) = build_interactor(build_line_group(20)); let err = interactor - .get_train_route(1002, 9999, Some(1000)) + .get_train_route(1002, 9999, Some(1000), model::TrainRouteModel::Legacy) .await .unwrap_err(); @@ -6345,7 +6503,7 @@ mod tests { let (interactor, _) = build_interactor(build_line_group(20)); let err = interactor - .get_train_route(1002, 1004, None) + .get_train_route(1002, 1004, None, model::TrainRouteModel::Legacy) .await .unwrap_err(); diff --git a/stationapi/src/use_case/traits/query.rs b/stationapi/src/use_case/traits/query.rs index 53cc5478..b598cd8d 100644 --- a/stationapi/src/use_case/traits/query.rs +++ b/stationapi/src/use_case/traits/query.rs @@ -9,7 +9,7 @@ use crate::{ }, route_search::JourneySort, }, - model::{ConnectedRoute, Route, RouteLegRequest, TrainRouteSegment}, + model::{ConnectedRoute, Route, RouteLegRequest, TrainRouteModel, TrainRouteSegment}, use_case::error::UseCaseError, }; @@ -111,6 +111,7 @@ pub trait QueryUseCase: Send + Sync + 'static { from_station_id: u32, to_station_id: u32, line_group_id: Option, + model: TrainRouteModel, ) -> Result, UseCaseError>; async fn find_line_by_id(&self, line_id: u32) -> Result, UseCaseError>; async fn get_lines_by_id_vec(&self, line_ids: &[u32]) -> Result, UseCaseError>; @@ -137,6 +138,7 @@ pub trait QueryUseCase: Send + Sync + 'static { async fn get_connected_train_route( &self, legs: &[RouteLegRequest], + model: TrainRouteModel, ) -> Result, UseCaseError>; async fn estimate_route_arrival_times( &self, From 3845c0035b2d2d7b1d00c1c2456079747223de96 Mon Sep 17 00:00:00 2001 From: Tsubasa SEKIGUCHI Date: Thu, 1 Oct 2026 03:26:32 +0900 Subject: [PATCH 2/5] =?UTF-8?q?=E5=88=B0=E7=9D=80=E6=99=82=E9=96=93?= =?UTF-8?q?=E6=8E=A8=E5=AE=9A=E3=82=92=E5=AE=9F=E9=9A=9B=E3=81=AE=E6=89=80?= =?UTF-8?q?=E8=A6=81=E6=99=82=E9=96=93=E3=81=A8=E6=AF=94=E3=81=B9=E3=82=8B?= =?UTF-8?q?=E3=83=99=E3=83=B3=E3=83=81=E3=83=9E=E3=83=BC=E3=82=AF=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0=E3=81=97=E3=80=81CI=E3=81=A7=E6=8E=A8?= =?UTF-8?q?=E5=AE=9A=E3=81=AE=E6=82=AA=E5=8C=96=E3=82=92=E6=A4=9C=E5=87=BA?= =?UTF-8?q?=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E3=81=97?= =?UTF-8?q?=E3=81=9F=20(#1711)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 到着時間推定を実際の所要時間と比べるベンチマークを追加し、CIで推定の悪化を検出できるようにした Co-Authored-By: Claude Opus 5.5 * 所要時間ベンチマークで記録済みの基準を推定できなくなったら失敗させ、generatedのデータでは記録と比べないようにした Co-Authored-By: Claude Opus 5.5 * 所要時間ベンチマークの記録を生成データで作り、build_worker.ymlで生成データを作ってから比べるようにした Co-Authored-By: Claude Opus 5.5 * 所要時間ベンチマークに典型的な所要時間の列を足し、回帰の見張りを範囲からの外れではなく典型値からのずれで判定するようにした Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- .github/workflows/build_worker.yml | 9 + AGENTS.md | 1 + Makefile | 10 +- README.md | 5 + build.rs | 10 + docs/architecture.md | 17 ++ scripts/travel_time_report.py | 109 ++++++++++ src/lib.rs | 2 + src/travel_times.rs | 311 +++++++++++++++++++++++++++++ travel_times/README.md | 77 +++++++ travel_times/baseline.csv | 19 ++ travel_times/cases.csv | 19 ++ 12 files changed, 588 insertions(+), 1 deletion(-) create mode 100644 scripts/travel_time_report.py create mode 100644 src/travel_times.rs create mode 100644 travel_times/README.md create mode 100644 travel_times/baseline.csv create mode 100644 travel_times/cases.csv diff --git a/.github/workflows/build_worker.yml b/.github/workflows/build_worker.yml index 2a7b2f7d..fa92c431 100644 --- a/.github/workflows/build_worker.yml +++ b/.github/workflows/build_worker.yml @@ -23,6 +23,7 @@ on: - "Cargo.toml" - ".github/actions/build-worker/action.yml" - ".github/workflows/build_worker.yml" + - "travel_times/**" push: # dev / master は deploy_staging.yml / deploy_production.yml が同じ # composite action で検証してからデプロイするため、ここでは走らせない。 @@ -42,6 +43,7 @@ on: - "Cargo.toml" - ".github/actions/build-worker/action.yml" - ".github/workflows/build_worker.yml" + - "travel_times/**" workflow_dispatch: name: Build Cloudflare Worker @@ -70,6 +72,13 @@ jobs: - uses: ./.github/actions/build-worker + # 到着時間推定の所要時間の見張り (src/travel_times.rs)。記録 + # (travel_times/baseline.csv) は本番と同じ生成データで作るので、上の + # action が generated/ を作ったこのジョブで比べる。バスのフィードが + # 欠けても鉄道の推定は変わらない。 + - name: Check travel times against the benchmark + run: cargo test -p stationapi-worker travel_times + - uses: actions/upload-artifact@v4 with: name: worker-build diff --git a/AGENTS.md b/AGENTS.md index 0a9ded7b..50795c94 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -64,6 +64,7 @@ The Worker is the workspace root package. `stationapi`, `preprocessor`, and `dat - **Schema** – Changing a GraphQL type changes the SDL. Update `schema/public.graphql` in the same change; CI compares it against the running Worker's `/__schema` and fails on any difference. That diff is exactly the client-visible impact. - **Data verification** – Execute `cargo run -p data_validator` whenever CSVs change and record results in pull requests. - **IPA coverage audit** – Execute `make ipa-audit` when English or romanized CSV names change. This is a read-only report for `data/2!lines.csv`, `data/3!stations.csv`, and `data/4!types.csv`; it does not fail validation, but highlights unresolved tokens and example names so the IPA dictionary can be extended deliberately. +- **Travel-time benchmark** – `travel_times/cases.csv` lists real travel times (a range and a typical value — the median — in minutes, weekday daytime, trains with the same stopping pattern as the line group) that the arrival estimation is measured against. `cargo test -p stationapi-worker` (`src/travel_times.rs`) fails when any case moves more than 1 percentage point further from its typical value than `travel_times/baseline.csv` records (a range alone hides drifts inside a range widened by one outlier train), when the mean gets worse, or when a recorded case can no longer be estimated. It compares only when the Worker embeds `generated/` (the estimation uses track lengths and line groups that exist only there), so `build_worker.yml` runs it after building the data; on `data/*.csv` it just prints the table. `make travel-time-report` (`TRAVEL_TIME_API`, default the local `make dev` Worker) measures every case against a Worker built from the generated data. A change to the speed tables, the general speed rules, or the estimation parameters must attach the report from before and after, and must not trade one line's accuracy for the whole set's; after an intended change, run `make data` and regenerate the baseline with `TRAVEL_TIMES_UPDATE_BASELINE=1 cargo test -p stationapi-worker travel_times`. Values come from open GTFS feeds (credit them in the README's Data Sources) or from the maintainer. - **Endpoint benchmarks** – `make bench` (or `python3 .claude/skills/benchmark-gql/bench.py`) replays every `Query` field against production (`gql.trainlcd.app`, script `stationapi`) and staging (`gql-stg.trainlcd.app`, script `stationapi-stg`) and writes a Markdown report under `benchmarks/`. Both environments embed the same data, so any difference is implementation — which makes this the way to see what a `dev`-to-`master` release will do to performance before it ships. Besides client latency it records the Worker's `cpuTime`, read from `wrangler tail --format json` and matched to each request by `cf-ray`; the tail is filtered on a per-run request header, so production's live traffic does not leak into the sample. Collecting CPU time needs the `workers_tail (read)` scope, and the run sends hundreds of real requests to production — it is not a routine check. Add a case to `.claude/skills/benchmark-gql/queries.json` whenever a `Query` field is added, and never edit an existing case's variables: the reports are meant to stay comparable across runs. ## GraphQL Query Overview diff --git a/Makefile b/Makefile index 779fd12b..f0153e94 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # StationAPI Makefile # よく使うタスクの定義 -.PHONY: help test check fmt clippy data build dev deploy deploy-production schema ipa-audit bench clean +.PHONY: help test check fmt clippy data build dev deploy deploy-production schema ipa-audit bench travel-time-report clean # CI (.github/workflows/build_worker.yml) と同じ版を使う。グローバルへ入れて # いなくても npx が取ってくるので、版ずれでビルド結果が変わらない。 @@ -22,6 +22,7 @@ help: @echo " schema - Diff the running Worker's SDL against schema/public.graphql" @echo " ipa-audit - Print IPA coverage report for English/romanized CSV names" @echo " bench - Compare production vs staging GraphQL performance (sends live traffic to both)" + @echo " travel-time-report - Compare estimated travel times with travel_times/cases.csv (TRAVEL_TIME_API, default http://127.0.0.1:8787/)" @echo " clean - Clean build artifacts" @echo "" @echo "Environment variables:" @@ -84,6 +85,13 @@ ipa-audit: # 実在のエンドポイントへ数百リクエスト投げるので、気軽に回すものではない。 # CPU Time の収集には wrangler の workers_tail (read) 権限が要る。 # 追加の引数は BENCH_ARGS で渡す (例: make bench BENCH_ARGS="--repeat 30")。 +# 到着時間推定の所要時間を、実際の所要時間 (travel_times/cases.csv) と比べる。 +# 本番と同じ生成データで測るため、`make data && make dev` で起動した Worker か +# ステージングに向ける (TRAVEL_TIME_API)。 +TRAVEL_TIME_API ?= http://127.0.0.1:8787/ +travel-time-report: + python3 scripts/travel_time_report.py --api "$(TRAVEL_TIME_API)" + bench: @echo "警告: 本番 (gql.trainlcd.app) とステージングへ実リクエストを送ります。" >&2 @echo " 既定で 1 環境あたり 400 件超、うち数十件は Worker の CPU を 500ms 以上使います。" >&2 diff --git a/README.md b/README.md index 12a26397..fc3a17de 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,11 @@ This project includes a comprehensive dataset of Japanese railway information in (https://nlftp.mlit.go.jp/ksj/gml/datalist/KsjTmplt-N02-2025.html) を加工して作成 - Bus stops and routes are derived from the GTFS and ODPT feeds listed in `preprocessor/src/gtfs/feed.rs` and `preprocessor/src/gtfs/odpt.rs`. +- Some of the reference travel times in `travel_times/cases.csv` are derived from + the Toei Subway GTFS (Bureau of Transportation, Tokyo Metropolitan Government, + CC BY 4.0) and from the Tokyo Metro and Metropolitan Intercity Railway + (Tsukuba Express) GTFS feeds published by the Public Transportation Open Data + Center under the Public Transportation Open Data Basic License. ## Contributors ✨ diff --git a/build.rs b/build.rs index 4fb2a683..2cdc543d 100644 --- a/build.rs +++ b/build.rs @@ -85,6 +85,16 @@ fn main() { staged.len() ); } + // どちらのデータを埋め込んだか。所要時間のベンチマーク (src/travel_times.rs) の + // 記録は生成データで作るので、data/*.csv のときは記録と比べない + println!( + "cargo:rustc-env=STATIONAPI_EMBEDDED_DATA={}", + if generated_count == 0 { + "data" + } else { + "generated" + } + ); if generated_count == 0 { println!( "cargo:warning=generated が無いため data/*.csv を使用します。\ diff --git a/docs/architecture.md b/docs/architecture.md index 3629a332..9ff2fd9f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -506,6 +506,23 @@ input RouteLegInput { lineGroupId: Int! fromStationId: Int! toStationId: Int! - 両端の駅が `fromStationId` / `toStationId` と一致しない - `viaLineIds`・`directionId`・`lineGroupId` と同時に指定されている +### 所要時間のベンチマーク (`travel_times/`) + +到着時間推定の所要時間を、実際の列車の所要時間と比べる基準を `travel_times/cases.csv` +に置いています。速度の較正テーブルや一般則は、1 つの路線に合わせて変えると、同じ +規則を使うほかの路線の推定も変わります。変更の前後で全体の誤差を測るための仕組み +です。 + +- `cargo test -p stationapi-worker` (`src/travel_times.rs`) は、基準ごとの「実際の + 典型的な所要時間からのずれ」を `travel_times/baseline.csv` の記録と比べ、悪くなる + と失敗します。記録は本番と同じ生成データで作るので、比べるのは `generated/` で + 動くときだけです。 + CI では `build_worker.yml` が生成データを作ってから走らせます。 +- `make travel-time-report` は、生成データで動く Worker に問い合わせて全件の誤差を + 出します。推定の規則や較正を変える PR には、変更前と変更後のレポートを載せます。 + +基準の決め方と記録の更新方法は `travel_times/README.md` にあります。 + ### 行き先の検索 (`stationsByName`) `stationsByName` に `fromStationGroupId` を指定すると、その駅から行ける駅だけに diff --git a/scripts/travel_time_report.py b/scripts/travel_time_report.py new file mode 100644 index 00000000..dd9fc437 --- /dev/null +++ b/scripts/travel_time_report.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +"""実際の所要時間 (travel_times/cases.csv) に対する到着時間推定の誤差を、動いている +Worker に問い合わせて Markdown で出す。 + +CI の回帰テスト (src/travel_times.rs) は data/*.csv だけで動くので、生成データにしか +無い種別グループを飛ばし、線路の長さも持たない。本番と同じ生成データでの精度は、 +`make data && make dev` で起動した Worker か、ステージングに向けてこれで測る。 +推定の規則や較正を変える PR には、変更前と変更後のこのレポートを載せる。 + +使い方: + python3 scripts/travel_time_report.py # http://127.0.0.1:8787/ + python3 scripts/travel_time_report.py --api +""" +from __future__ import annotations + +import argparse +import csv +import json +import statistics +import sys +import urllib.request +from pathlib import Path + +CASES = Path(__file__).resolve().parent.parent / "travel_times" / "cases.csv" +# 既定の Python-urllib は配信側で弾かれるので、bench.py と同じく名乗る +USER_AGENT = "stationapi-travel-time-report/1.0 (+https://github.com/TrainLCD/StationAPI)" +QUERY = """query TravelTimeReport($from: Int!, $to: Int!, $group: Int!) { + estimateArrivalTimes(fromStationId: $from, toStationId: $to, + legs: [{ lineGroupId: $group, fromStationId: $from, toStationId: $to }]) { + routes { stops { stationId cumulativeMinutes departureCumulativeMinutes } } + } +}""" + + +def estimate(api: str, case: dict) -> float: + end = int(case["slice_end_station_id"] or case["to_station_id"]) + body = json.dumps({ + "query": QUERY, + "variables": { + "from": int(case["from_station_id"]), + "to": end, + "group": int(case["line_group_id"]), + }, + }).encode() + req = urllib.request.Request( + api, data=body, headers={"content-type": "application/json", "user-agent": USER_AGENT} + ) + with urllib.request.urlopen(req, timeout=30) as res: + data = json.load(res) + if data.get("errors"): + raise RuntimeError("; ".join(e.get("message", "") for e in data["errors"])) + stops = data["data"]["estimateArrivalTimes"]["routes"][0]["stops"] + target = int(case["to_station_id"]) + stop = next(s for s in stops[1:] if s["stationId"] == target) + key = "cumulativeMinutes" if case["measure"] == "arrival" else "departureCumulativeMinutes" + return float(stop[key]) + + +def range_error(est: float, lo: float, hi: float) -> float: + if est < lo: + return (lo - est) / lo + if est > hi: + return (est - hi) / hi + return 0.0 + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__.split("\n")[0]) + parser.add_argument("--api", default="http://127.0.0.1:8787/") + args = parser.parse_args() + + with CASES.open(encoding="utf-8") as f: + cases = list(csv.DictReader(f)) + + rows, range_errors, typical_errors, failed = [], [], [], [] + for case in cases: + lo, hi = float(case["real_min_minutes"]), float(case["real_max_minutes"]) + typical = float(case["real_typical_minutes"]) + try: + est = estimate(args.api, case) + except Exception as e: # noqa: BLE001 - 1 件の失敗で全体を止めない + failed.append(f"{case['label']}: {e}") + continue + t, r = (est - typical) / typical, range_error(est, lo, hi) + typical_errors.append(abs(t)) + range_errors.append(r) + rows.append( + f"| {case['label']} | {typical:g}分 ({lo:g}〜{hi:g}分) | {est:.1f}分 " + f"| {t * 100:+.1f}% | {r * 100:.1f}% |" + ) + + print(f"# 到着時間推定の誤差 ({args.api})\n") + print("| 基準 | 実際の典型 (範囲) | 推定 | 典型からのずれ | 範囲からの外れ |") + print("| --- | --- | --- | --- | --- |") + print("\n".join(rows)) + if typical_errors: + print( + f"\n{len(typical_errors)} 件: 典型からのずれ (絶対値) の平均 " + f"{statistics.mean(typical_errors) * 100:.2f}%、" + f"範囲からの外れの平均 {statistics.mean(range_errors) * 100:.2f}%" + ) + if failed: + print("\n推定できなかった基準:\n") + print("\n".join(f"- {line}" for line in failed)) + return 0 if typical_errors else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/lib.rs b/src/lib.rs index f7f850f4..f7a140c7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,8 @@ mod graphql; mod index; mod repository; +#[cfg(test)] +mod travel_times; use async_graphql::http::GraphiQLSource; use async_graphql::Request as GqlRequest; diff --git a/src/travel_times.rs b/src/travel_times.rs new file mode 100644 index 00000000..1c6aeff3 --- /dev/null +++ b/src/travel_times.rs @@ -0,0 +1,311 @@ +//! 実際の所要時間 (`travel_times/cases.csv`) に対する到着時間推定の回帰の見張り。 +//! +//! 基準ごとに推定の所要時間を出し、実際の典型的な所要時間 (平日日中の中央値) から +//! のずれを求める。記録した推定 (`travel_times/baseline.csv`) より悪くなった基準が +//! あるか、平均が悪くなったら失敗にする。速度の較正や一般則を 1 つの路線に合わせて +//! 変えたときに、ほかの路線がどれだけ崩れたかをここで見る。 +//! +//! 実際の所要時間の範囲 (最小〜最大) からの外れも表に出すが、判定には使わない。 +//! 待ち合わせなどで範囲に外れ値の列車が入ると幅が広がり、範囲内なら誤差 0 と +//! 数える物差しでは、典型的な値から大きく離れても見逃すため。 +//! +//! 記録は、本番と同じ生成データ (`make data` で作る `generated/`) で出した推定で、 +//! 比べるのも生成データのときだけにする。到着時間推定は、生成データにしか無い +//! 線路の長さや種別グループを使うので、`data/*.csv` だけでは本番の推定を再現 +//! できない。CI では `build_worker.yml` が `generated/` を作ってからこれを走らせる。 +//! `data/*.csv` で動くとき (`ci.yml` のテストなど) は、表を出すだけにする。 +//! +//! 推定を意図して変えたときは、`make data` のあとに次で記録を更新する。 +//! `TRAVEL_TIMES_UPDATE_BASELINE=1 cargo test -p stationapi-worker travel_times` + +use std::collections::HashMap; + +use stationapi::domain::repository::station_repository::StationRepository; +use stationapi::model::RouteLegRequest; +use stationapi::use_case::traits::query::QueryUseCase; + +use crate::repository::MemStationRepository; + +const CASES: &str = include_str!("../travel_times/cases.csv"); +const BASELINE_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/travel_times/baseline.csv"); +/// 1 基準あたり、典型的な値からのずれがこれだけ増えたら失敗にする (割合) +const CASE_TOLERANCE: f64 = 0.01; +/// 平均の比較の許容幅 (割合)。記録は推定の分を小数 4 桁に丸めて書くので、その丸めで +/// 平均がわずかに動くぶんを吸収する +const MEAN_TOLERANCE: f64 = 1e-4; + +#[derive(Debug, Clone, Copy, PartialEq)] +enum Measure { + /// 出発駅の発車から到着駅の到着まで + Arrival, + /// 出発駅の発車から到着駅の発車まで (到着時刻を載せない時刻表向け) + Departure, +} + +#[derive(Debug)] +struct Case { + label: String, + line_group_id: u32, + from_station_id: u32, + to_station_id: u32, + /// 推定する区間の終わり。`Departure` では到着駅の先まで推定しないと + /// 到着駅が終点になり、停車時間が付かない + slice_end_station_id: u32, + measure: Measure, + real_min: f64, + real_max: f64, + /// 典型的な所要時間 (平日日中の中央値。分からないときは範囲の中央) + real_typical: f64, +} + +impl Case { + /// 典型的な所要時間からのずれ (絶対値の割合)。判定に使う指標 + fn typical_error(&self, estimated: f64) -> f64 { + (estimated - self.real_typical).abs() / self.real_typical + } + + /// 実際の範囲からの外れ。範囲内なら 0、外れたら近い端に対する割合 + fn range_error(&self, estimated: f64) -> f64 { + if estimated < self.real_min { + (self.real_min - estimated) / self.real_min + } else if estimated > self.real_max { + (estimated - self.real_max) / self.real_max + } else { + 0.0 + } + } +} + +fn parse_cases() -> Vec { + let mut lines = CASES.lines(); + let header: Vec<&str> = lines.next().expect("見出しの行が無い").split(',').collect(); + let col = |name: &str| { + header + .iter() + .position(|h| *h == name) + .unwrap_or_else(|| panic!("列 {name} が無い")) + }; + let (label, group, from, to, end, measure, min, max, typical) = ( + col("label"), + col("line_group_id"), + col("from_station_id"), + col("to_station_id"), + col("slice_end_station_id"), + col("measure"), + col("real_min_minutes"), + col("real_max_minutes"), + col("real_typical_minutes"), + ); + lines + .filter(|line| !line.trim().is_empty()) + .map(|line| { + let f: Vec<&str> = line.split(',').collect(); + assert_eq!(f.len(), header.len(), "列の数が見出しと違う: {line}"); + let num = |i: usize| -> u32 { + f[i].parse() + .unwrap_or_else(|_| panic!("数値ではない: {line}")) + }; + let minutes = |i: usize| -> f64 { + f[i].parse() + .unwrap_or_else(|_| panic!("数値ではない: {line}")) + }; + let measure = match f[measure] { + "arrival" => Measure::Arrival, + "departure" => Measure::Departure, + other => panic!("measure は arrival / departure のどちらか: {other}"), + }; + let to_station_id = num(to); + Case { + label: f[label].to_string(), + line_group_id: num(group), + from_station_id: num(from), + to_station_id, + slice_end_station_id: if f[end].is_empty() { + to_station_id + } else { + num(end) + }, + measure, + real_min: minutes(min), + real_max: minutes(max), + real_typical: minutes(typical), + } + }) + .collect() +} + +/// repository の実装は await しない (索引を引くだけ) ので、1 回 poll すれば終わる +fn block_on(future: F) -> F::Output { + let mut context = std::task::Context::from_waker(std::task::Waker::noop()); + match std::pin::pin!(future).poll(&mut context) { + std::task::Poll::Ready(value) => value, + std::task::Poll::Pending => panic!("repository futures complete without waiting"), + } +} + +/// 推定の所要時間 (分)。種別グループがこのデータに無ければ `None` +fn estimate(case: &Case) -> Option { + let group = block_on(MemStationRepository.get_by_line_group_id(case.line_group_id)).ok()?; + if group.is_empty() { + return None; + } + let legs = [RouteLegRequest { + line_group_id: case.line_group_id, + from_station_id: case.from_station_id, + to_station_id: case.slice_end_station_id, + }]; + let stops = block_on(crate::interactor().estimate_connected_route_arrival_times(&legs)) + .unwrap_or_else(|e| panic!("{}: 推定できない: {e}", case.label)); + let stop = stops + .iter() + .skip(1) + .find(|stop| stop.station_cd as u32 == case.to_station_id) + .unwrap_or_else(|| panic!("{}: 推定の駅列に到着駅が無い", case.label)); + Some(match case.measure { + Measure::Arrival => stop.cumulative_minutes, + Measure::Departure => stop.departure_cumulative_minutes, + }) +} + +fn read_baseline() -> HashMap { + let text = std::fs::read_to_string(BASELINE_PATH).unwrap_or_default(); + text.lines() + .skip(1) + .filter(|line| !line.trim().is_empty()) + .map(|line| { + let (label, minutes) = line.rsplit_once(',').expect("label,estimated_minutes の形"); + ( + label.to_string(), + minutes.parse().expect("推定の分が数値ではない"), + ) + }) + .collect() +} + +#[test] +fn cases_are_well_formed() { + let cases = parse_cases(); + assert!(!cases.is_empty()); + let mut labels = std::collections::HashSet::new(); + for case in &cases { + assert!(labels.insert(&case.label), "label が重複: {}", case.label); + assert!( + case.real_min > 0.0 + && case.real_min <= case.real_typical + && case.real_typical <= case.real_max, + "{}", + case.label + ); + if case.measure == Measure::Departure { + assert_ne!( + case.slice_end_station_id, case.to_station_id, + "{}: departure は到着駅の先まで推定する (slice_end_station_id)", + case.label + ); + } + } +} + +#[test] +fn estimates_do_not_drift_away_from_real_travel_times() { + let cases = parse_cases(); + let estimated: Vec<(&Case, Option)> = cases.iter().map(|c| (c, estimate(c))).collect(); + + let mut report = vec![String::from( + "\n基準 | 実際 (典型) | 推定 | 典型からのずれ | 範囲からの外れ", + )]; + for (case, est) in &estimated { + let real = format!( + "{}〜{}分 ({}分)", + case.real_min, case.real_max, case.real_typical + ); + report.push(match est { + Some(est) => format!( + "{} | {real} | {est:.1}分 | {:.1}% | {:.1}%", + case.label, + case.typical_error(*est) * 100.0, + case.range_error(*est) * 100.0 + ), + None => format!( + "{} | {real} | (このデータに種別グループが無い) | - | -", + case.label + ), + }); + } + println!("{}", report.join("\n")); + + // 記録は本番と同じ生成データで作る。data/*.csv では線路の長さや生成された + // 種別グループが無く、推定が本番と違うので、記録とは比べない + let embedded_generated = env!("STATIONAPI_EMBEDDED_DATA") == "generated"; + + if std::env::var("TRAVEL_TIMES_UPDATE_BASELINE").as_deref() == Ok("1") { + assert!( + embedded_generated, + "記録は生成データで作る。make data で generated/ を作ってから更新する" + ); + let mut out = String::from("label,estimated_minutes\n"); + for (case, est) in &estimated { + if let Some(est) = est { + out.push_str(&format!("{},{est:.4}\n", case.label)); + } + } + std::fs::write(BASELINE_PATH, out).expect("記録を書き込めない"); + return; + } + + if !embedded_generated { + println!( + "data/*.csv で動いているので記録とは比べない。\ + make data で generated/ を作ると記録と比べる" + ); + return; + } + + let baseline = read_baseline(); + let (mut now_sum, mut base_sum, mut n) = (0.0, 0.0, 0); + let mut worse = Vec::new(); + for (case, est) in &estimated { + // 推定できない基準は記録にも入らないので飛ばしてよい。記録済みの基準を + // 推定できなくなったのは、データの変更で種別グループが消えたなどの異常 + // なので、黙って比較から外さずに止める + let Some(est) = est else { + assert!( + !baseline.contains_key(&case.label), + "{}: 記録済みの基準を推定できない (種別グループがデータから消えた可能性)", + case.label + ); + continue; + }; + let base = baseline.get(&case.label).unwrap_or_else(|| { + panic!( + "{}: 記録が無い。TRAVEL_TIMES_UPDATE_BASELINE=1 で記録を更新する", + case.label + ) + }); + let (now_err, base_err) = (case.typical_error(*est), case.typical_error(*base)); + if now_err > base_err + CASE_TOLERANCE { + worse.push(format!( + "{}: 典型的な所要時間からのずれが {:.1}% → {:.1}% (推定 {base:.1}分 → {est:.1}分)", + case.label, + base_err * 100.0, + now_err * 100.0 + )); + } + now_sum += now_err; + base_sum += base_err; + n += 1; + } + assert!(n > 0, "このデータで推定できる基準が 1 つも無い"); + assert!( + worse.is_empty(), + "実際の所要時間から離れた基準がある:\n{}", + worse.join("\n") + ); + let (now_mean, base_mean) = (now_sum / n as f64, base_sum / n as f64); + assert!( + now_mean <= base_mean + MEAN_TOLERANCE, + "典型的な所要時間からのずれの平均が {:.2}% → {:.2}% に悪化した", + base_mean * 100.0, + now_mean * 100.0 + ); +} diff --git a/travel_times/README.md b/travel_times/README.md new file mode 100644 index 00000000..f902f420 --- /dev/null +++ b/travel_times/README.md @@ -0,0 +1,77 @@ +# travel_times/ + +到着時間推定 (`stationapi/src/domain/arrival_estimation.rs`) の所要時間を、実際の +列車の所要時間と比べるための基準を置く場所です。速度の較正テーブルや一般則は、 +1 つの路線に合わせて変えると、同じ規則を使うほかの路線の推定も変わります。 +変更の前後で全体の誤差を測り、局所的な合わせ込みで全体が崩れないようにします。 + +## ファイル + +| パス | 内容 | +| --- | --- | +| `cases.csv` | 基準の一覧。1 行が 1 区間 | +| `baseline.csv` | 本番と同じ生成データ (`make data` で作る `generated/`) で出した推定の所要時間の記録 | + +`cases.csv` の列は次のとおりです。 + +| 列 | 内容 | +| --- | --- | +| `label` | 基準の名前。一覧の中で重複させない | +| `line_group_id` | 推定に使う種別グループ (系統) | +| `from_station_id` / `to_station_id` | 区間の出発駅と到着駅 | +| `slice_end_station_id` | 推定する区間の終わり。`measure` が `departure` のときに、到着駅より先の駅を書く。空なら到着駅 | +| `measure` | `arrival` は出発駅の発車から到着駅の到着まで。`departure` は到着駅の発車までで、到着時刻を載せない時刻表の値に使う | +| `real_min_minutes` / `real_max_minutes` | 実際の所要時間の範囲 (分) | +| `real_typical_minutes` | 実際の典型的な所要時間 (分)。回帰の見張りはこの値からのずれで判定する | +| `source` | 値の出どころ | + +## 基準の決め方 + +- 平日の日中に出発駅を出る列車の所要時間を使います。途中で待ち合わせる列車などで + 所要時間に幅があるときは、最小と最大をそのまま書きます。 +- 典型的な所要時間には、同じ列車の所要時間の中央値を書きます。中央値が分からず + 範囲だけが分かっているときは範囲の中央を書き、`source` にそう書き添えます。 + 範囲は外れ値の列車 1 本で広がるので、範囲に入っているかどうかだけでは、推定が + 典型的な値から離れたことを見逃します。 +- 列車は、`line_group_id` の種別グループと同じ停車パターンのものに限ります。 + 停車駅が違う列車の所要時間と比べると、推定の誤差ではない差が混ざります。 +- 出どころは、公開 GTFS か、メンテナが確認した値に限ります。GTFS から求めた値を + 足すときは、そのフィードの出典をリポジトリ直下の README の「Data Sources」に + 書きます。 + +## 測る + +### CI (回帰の見張り) + +`cargo test -p stationapi-worker` の `travel_times` モジュール (`src/travel_times.rs`) +が、基準ごとに推定を出し、典型的な所要時間からのずれ (絶対値の割合) を求めます。 +`baseline.csv` の記録と比べて、1 件でもずれが 1 ポイントより多く増えるか、平均が +悪くなると失敗します。範囲からの外れ (範囲内なら 0、外れたら近い端に対する割合) +も表に出しますが、判定には使いません。記録にある基準を +推定できなくなったとき (種別グループがデータから消えたときなど) も失敗します。 + +比べるのは、本番と同じ生成データ (`generated/`) で動くときだけです。到着時間推定は、 +生成データにしか無い線路の長さや種別グループを使うので、`data/*.csv` だけでは本番の +推定を再現できません。CI では `build_worker.yml` が `generated/` を作ってから走らせ +ます。`data/*.csv` で動くとき (`ci.yml` のテストなど) は、表を出すだけにします。 + +推定を意図して変えたときは、`make data` のあとに記録を更新して、同じ PR に含めます。 +`generated/` が無いと更新は止まります。 + +```bash +make data +TRAVEL_TIMES_UPDATE_BASELINE=1 cargo test -p stationapi-worker travel_times +``` + +### レポート (本番と同じデータでの精度) + +動いている Worker に `estimateArrivalTimes` を問い合わせ、全件の誤差を Markdown で +出します。`make data && make dev` で起動した Worker (既定) か、ステージングに +向けます。 + +```bash +make travel-time-report +make travel-time-report TRAVEL_TIME_API=https://gql-stg.trainlcd.app/ +``` + +推定の規則や較正を変える PR には、変更前と変更後のレポートを載せます。 diff --git a/travel_times/baseline.csv b/travel_times/baseline.csv new file mode 100644 index 00000000..0bfaf088 --- /dev/null +++ b/travel_times/baseline.csv @@ -0,0 +1,19 @@ +label,estimated_minutes +総武快速線 快速 錦糸町→津田沼,22.4690 +総武快速線 快速 新小岩→津田沼,17.3157 +京成 スカイライナー 日暮里→空港第2ビル,42.1152 +京急本線 快特 品川→横浜,17.6754 +小田急小田原線 快速急行 新宿→町田,32.8432 +東急東横線 特急 渋谷→横浜,26.4775 +京王井の頭線 急行 渋谷→吉祥寺,16.8168 +京王線 特急 新宿→京王八王子,41.0112 +阪急神戸本線 特急 大阪梅田→神戸三宮,26.6167 +西武池袋線 準急 池袋→所沢,29.4847 +西武池袋線 急行 池袋→所沢,22.4978 +都営大江戸線 落合南長崎→光が丘,11.5568 +都営大江戸線 新宿→光が丘,24.8605 +都営大江戸線 光が丘→都庁前,22.6198 +都営大江戸線 清澄白河→赤羽橋,16.0367 +都営大江戸線 光が丘→都庁前 (環状部経由),84.3894 +東京メトロ銀座線 渋谷→新橋,13.2494 +つくばエクスプレス 普通 秋葉原→つくば,61.8611 diff --git a/travel_times/cases.csv b/travel_times/cases.csv new file mode 100644 index 00000000..4f722854 --- /dev/null +++ b/travel_times/cases.csv @@ -0,0 +1,19 @@ +label,line_group_id,from_station_id,to_station_id,slice_end_station_id,measure,real_min_minutes,real_max_minutes,real_typical_minutes,source +総武快速線 快速 錦糸町→津田沼,40,1131404,1131408,1131410,departure,19,25,20,メンテナ確認 +総武快速線 快速 新小岩→津田沼,40,1131405,1131408,1131410,departure,15,20,16,メンテナ確認 +京成 スカイライナー 日暮里→空港第2ビル,197,2300102,2300610,,arrival,40,48,41,メンテナ確認 +京急本線 快特 品川→横浜,74,2700102,2700126,,arrival,17,22,17,メンテナ確認 +小田急小田原線 快速急行 新宿→町田,129,2500101,2500127,,arrival,30,31,30.5,メンテナ確認 (典型値は範囲の中央) +東急東横線 特急 渋谷→横浜,161,2600101,2600121,,arrival,27,28,27.5,メンテナ確認 (典型値は範囲の中央) +京王井の頭線 急行 渋谷→吉祥寺,72,2400601,2400617,,arrival,17,22,17,メンテナ確認 +京王線 特急 新宿→京王八王子,71,2400101,2400134,,arrival,45,45,45,メンテナ確認 +阪急神戸本線 特急 大阪梅田→神戸三宮,384,3400101,3400116,,arrival,27,28,27.5,メンテナ確認 (典型値は範囲の中央) +西武池袋線 準急 池袋→所沢,132,2200101,2200131,,arrival,30,38,38,メンテナ確認 +西武池袋線 急行 池袋→所沢,190,2200101,2200131,,arrival,21,25,24,メンテナ確認 +都営大江戸線 落合南長崎→光が丘,1000099301,9930133,9930138,,arrival,11,12,11,東京都交通局 GTFS +都営大江戸線 新宿→光が丘,1000099301,9930128,9930138,,arrival,24,26,24,東京都交通局 GTFS +都営大江戸線 光が丘→都庁前,1000099301,9930138,9930101,,arrival,21,21,21,東京都交通局 GTFS +都営大江戸線 清澄白河→赤羽橋,1000099301,9930115,9930122,,arrival,16,17,16,東京都交通局 GTFS +都営大江戸線 光が丘→都庁前 (環状部経由),1000099301,9930138,9930100,,arrival,84,86,84,東京都交通局 GTFS +東京メトロ銀座線 渋谷→新橋,1137,2800119,2800112,,arrival,15,15,15,東京メトロ GTFS +つくばエクスプレス 普通 秋葉原→つくば,977,9930901,9930920,,arrival,63,66,66,首都圏新都市鉄道 GTFS From 5f13885c4152f90fe8fd5199d5cc419f8d217a9c Mon Sep 17 00:00:00 2001 From: Tsubasa SEKIGUCHI Date: Thu, 1 Oct 2026 03:46:12 +0900 Subject: [PATCH 3/5] =?UTF-8?q?=E5=88=B0=E7=9D=80=E6=99=82=E9=96=93?= =?UTF-8?q?=E6=8E=A8=E5=AE=9A=E3=81=A8=E4=B9=97=E6=8F=9B=E7=B5=8C=E8=B7=AF?= =?UTF-8?q?=E6=8E=A2=E7=B4=A2=E3=81=AE=E9=A7=85=E9=96=93=E8=B7=9D=E9=9B=A2?= =?UTF-8?q?=E3=81=AB=E7=B7=9A=E8=B7=AF=E3=81=AE=E9=95=B7=E3=81=95=E3=82=92?= =?UTF-8?q?=E4=BD=BF=E3=81=84=E3=80=81=E9=80=9F=E5=BA=A6=E3=81=AE=E8=BC=83?= =?UTF-8?q?=E6=AD=A3=E3=82=92=E3=81=9D=E3=81=AE=E8=B7=9D=E9=9B=A2=E3=81=A7?= =?UTF-8?q?=E6=B1=82=E3=82=81=E7=9B=B4=E3=81=97=E3=81=A6=E3=80=81=E3=82=AA?= =?UTF-8?q?=E3=83=BC=E3=83=88=E3=83=A2=E3=83=BC=E3=83=89=E3=81=AE=E5=80=A4?= =?UTF-8?q?=E3=81=AF=E5=87=8D=E7=B5=90=E3=81=97=E3=81=9F=E8=A1=A8=E3=81=A7?= =?UTF-8?q?=E4=BF=9D=E3=81=A4=E3=82=88=E3=81=86=E3=81=AB=E3=81=97=E3=81=9F?= =?UTF-8?q?=20(#1712)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Claude Opus 5.5 --- AGENTS.md | 4 +- README.md | 7 +- docs/architecture.md | 11 +- scripts/README.md | 5 + scripts/compute_speed_table.py | 50 +- src/repository.rs | 3 +- stationapi/src/domain.rs | 1 + stationapi/src/domain/arrival_estimation.rs | 119 ++- stationapi/src/domain/legacy_speed_table.rs | 934 +++++++++++++++++++ stationapi/src/domain/route_search.rs | 58 +- stationapi/src/domain/segment_speed_table.rs | 904 +++++++++--------- stationapi/src/domain/speed_table.rs | 108 ++- stationapi/src/use_case/dto/simulation.rs | 7 +- stationapi/src/use_case/interactor/query.rs | 114 ++- travel_times/baseline.csv | 39 +- travel_times/cases.csv | 3 + 16 files changed, 1759 insertions(+), 608 deletions(-) create mode 100644 stationapi/src/domain/legacy_speed_table.rs diff --git a/AGENTS.md b/AGENTS.md index 50795c94..39922b06 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -71,14 +71,14 @@ The Worker is the workspace root package. `stationapi`, `preprocessor`, and `dat - **Stations** – `station`, `stations`, `stationGroupStations`, `stationsNearby`, `lineStations`, `stationsByName`, `lineGroupStations`, `lineListStations`, `lineGroupListStations`. `QueryInteractor` enriches stations with lines, companies, station numbers, and train types. `lineStations` resolves the line's local train-type group (rail `kind` 0/1 or a `priority > 0` type; a bullet-train line, which has no local service, takes the group of its stopping train type with the smallest `types.id`, e.g. Nozomi or Hayabusa) and returns its stops carrying that group's train type, with or without `stationId`, so a client that never picked a train type still has the `lineGroupId` `trainRoute` requires; when no such group exists — bus lines only carry `BusRoute` (`kind` 7, `priority` 0) variants — it falls back to the line's plain typeless station list so bus stop listings never return empty. `stationsByName` with `fromStationGroupId` returns the stations reachable from there: stations sharing a line group with the origin (`line_group_cd` set, `has_train_types` true), same-line stations when either side has no line group, and — for rail — stations reachable by transferring, i.e. those for which `connectedRoutes` with `viaLineId` set to the station's line returns a route (`line_group_cd` empty, `has_train_types` false). The transfer check uses `RouteTopology` (`stationapi/src/domain/route_topology.rs`), a time-free copy of the `connectedRoutes` network built straight from the index without `Station` entities or time estimates (about 20 ms instead of about 190 ms, cached in its own `OnceLock`); `RouteNetwork` holds the same topology, both share `trim_pattern` and `line_group_rows`, and a real-data test asserts the two are equal. The check is a ride-limited BFS over line groups (a few ms) plus, only for destinations that are cut vertices of the station–line-group graph, a check that arrives without stopping over at the destination group — otherwise a branch's junction station (Ishibashi-handai-mae on the Minoo Line) would be listed although reaching it on that branch means riding out and back. - **Lines** – `line`, `lines`, `linesByName`. Results include company data and computed line symbols based on repository helpers. - **Routes** – `routes`, `connectedRoutes`, `estimateArrivalTimes`, `trainRoute`. Paging tokens are currently empty (pagination not implemented). -- **`trainRoute`** – Takes the line group's stops from the repository *before* any enrichment, slices them to the requested `fromStationId`–`toStationId` range (reversing when the request runs backwards), and only then attaches lines, companies, station numbers, train types, and nearby bus routes. Enrichment is per-station and independent, so slicing first does not change any segment; enriching the whole line group first made a three-station request cost the same as a 250-station one. Keep the order — the cost of this query must stay proportional to the requested range, not to the line group. The optional `model` argument (`TrainRouteModel`) picks how the segment values are computed. `Legacy`, the default, keeps the values from when the query was added (#1568): `dto::simulation::resolve_speed_profile` supplies the speed and acceleration, and `arrivalCumulativeMinutes` / `departureCumulativeMinutes` stay `null`. TrainLCD/MobileApp's auto mode depends on these values, so do not change them. `Estimated` runs `arrival_estimation` — the model the speed tables are calibrated against — on the same pre-enrichment slice (with `legs`, it reuses `estimate_connected_route_arrival_times`, so the values equal `estimateArrivalTimes` given the same `legs`). It then replaces each segment's `stops`, speed, and acceleration with the estimator's and fills the two cumulative fields. A route containing a bus station keeps the `Legacy` values, because the estimator's kinematics do not apply to buses. +- **`trainRoute`** – Takes the line group's stops from the repository *before* any enrichment, slices them to the requested `fromStationId`–`toStationId` range (reversing when the request runs backwards), and only then attaches lines, companies, station numbers, train types, and nearby bus routes. Enrichment is per-station and independent, so slicing first does not change any segment; enriching the whole line group first made a three-station request cost the same as a 250-station one. Keep the order — the cost of this query must stay proportional to the requested range, not to the line group. The optional `model` argument (`TrainRouteModel`) picks how the segment values are computed. `Legacy`, the default, keeps the values from when the query was added (#1568): `dto::simulation::resolve_speed_profile` supplies the speed and acceleration, and `arrivalCumulativeMinutes` / `departureCumulativeMinutes` stay `null`. TrainLCD/MobileApp's auto mode depends on these values, so do not change them; `Legacy` reads frozen copies of the speed tables (`domain/legacy_speed_table.rs`) so that recalibrating the arrival estimation does not move it. `Estimated` runs `arrival_estimation` — the model the speed tables are calibrated against — on the same pre-enrichment slice (with `legs`, it reuses `estimate_connected_route_arrival_times`, so the values equal `estimateArrivalTimes` given the same `legs`). It then replaces each segment's `stops`, speed, and acceleration with the estimator's and fills the two cumulative fields. A route containing a bus station keeps the `Legacy` values, because the estimator's kinematics do not apply to buses. - **Coordinate lookups** – `index::nearest` (k nearest, used by `stationsNearby`) and `index::within_radius` (everything inside a radius, used by the nearby-bus-stop enrichment) both go through a per-transport-type grid index (`Grid`, CSR over 0.05° cells) instead of scanning the whole station table. `nearest` searches a radius, widens it while fewer than `limit` stations fall inside, and stops once the radius covers the index — anything outside a radius that already holds `limit` hits cannot be in the top `limit`. With `transportType` omitted it returns rail stations first and bus stops after, each group sorted by distance — the pre-Workers SQL's `ORDER BY transport_type, distance`. The limit applies to the merged order, so `nearest` fills it with rail and only asks the bus grid for the remaining slots; a location with `limit` rail stations returns no bus stops at all. Ties on distance break on `station_cd` so the order does not depend on an unstable sort. Every station lookup by coordinates runs on every request that enriches rail stations with nearby bus routes, so keep new coordinate queries on the grid rather than adding another full scan. - **Train types** – `stationTrainTypes`, `routeTypes`. Train types aggregate by line group and include related lines plus optional train type metadata. Rail variants use `TrainTypeKind::{Default, Branch, Rapid, Express, LimitedExpress, HighSpeedRapid, CommuterRapid}` (0-6); bus variants use `BusRoute` (7), which represents a `(route_id, shape_id)` operation pattern (e.g. 循環 / 短ターン / 支線) generated automatically from the configured GTFS bus feeds (Toei Bus, Seibu Bus, Keio Bus) and the converted Tokyu Bus JSON. - **Default rail train types** – `preprocessor` fills every active rail line containing at least one station with no `station_station_types` row with a deterministic, complete all-stop group. The generated rows exist only in `generated/*.csv`; canonical CSV files remain unchanged. `type_cd=100` represents 「普通」 and `type_cd=101` represents 「各駅停車」. An existing 100/101 assignment on the line takes precedence; otherwise the label is selected per line through `LOCAL_SERVICE_RAIL_LINE_IDS` in `preprocessor/src/rail.rs`. Generated `line_group_cd` values use `1,000,000,000 + line_cd`; generation fails on a collision. Bus lines are excluded and continue to use their GTFS-derived `BusRoute` groups. - **GTFS bus integration** – `preprocessor/src/gtfs/` reads the GTFS feeds into an in-memory representation and then projects them onto the shared `stations` / `lines` / `types` / `station_station_types` tables (`gtfs/integrate.rs`). Only routes, stops, trips, and stop_times are read; calendar, shapes, feed_info, and agencies do not affect the output. Every configured GTFS feed is imported, including Seibu Bus and Keio Bus (both downloaded from ODPT with `ODPT_ACCESS_TOKEN`). Tokyu Bus ordinary-route `BusroutePattern`, `BusstopPole`, and `BusTimetable` JSON are converted into the same representation; pattern IDs become `shape_id` values so route variants remain queryable as bus TrainTypes. The Tokyu-operated Ota, Shinagawa, and Meguro community buses use their official GTFS feeds and matching JSON routes are excluded to prevent duplicates. `ODPT_ACCESS_TOKEN` is required for authenticated sources; without it those feeds are skipped with a warning rather than failing the build. Stops whose Tokyu JSON records omit coordinates remain available to name and route queries but not coordinate searches. `transport_type` (0: rail, 1: bus) on both `stations` and `lines` keeps rail and bus records queryable side by side. GTFS IDs are namespaced per feed before import to avoid cross-operator collisions. `line_cd` (100,000,000+), `station_cd` / `station_g_cd` (200,000,000+), and bus `type_cd` / `line_group_cd` (100,000,000+) are all deterministic fnv1a hashes that stay clear of the rail data ranges. Disable the entire bus pipeline with `DISABLE_BUS_FEATURE=true`. - **Bus stop translations (readings & English)** – GTFS-JP `translations.txt` layouts differ per feed, so `load_translations` (`preprocessor/src/gtfs/parse.rs`) resolves columns by header name (Seibu ships 6 columns without `record_sub_id`; Keio and the Tokyu community feeds ship 7) and indexes each `stop_name` translation under both keys it may use: `record_id` (== the stop_id, Seibu — with the "-NN" pole suffix also mapped to the parent stop_id) and `field_value` (== the Japanese stop_name, Keio / Tokyu community, where `record_id` is left empty). `load_stops` then looks a stop's translation up by stop_id first, then by name. Keying only by `record_id` would silently drop every field_value-keyed feed, leaving `station_name_k` filled with the kanji stop_name and `station_name_r` empty. Readings arriving as half-width katakana (`ニシハチオウジ`, Keio / Tokyu community) are folded to full-width via `romaji::to_fullwidth_katakana()` before storage. - **Bus English-name fallback** – When a feed provides no English (`en`) translation for a stop — e.g. Tokyu Bus ordinary-route JSON, which carries only `dc:title` and `odpt:kana` — `stationapi/src/domain/romaji.rs::romaji_display_name()` derives a modified-Hepburn romanization (with macrons for long vowels, matching the curated rail style: Tōkyō / Kyōto / Shin-Ōsaka) from the kana reading, and the GTFS reader fills `stop_name_r` with it. The fallback never overwrites a real `en` value, and a reading with no convertible kana stays `NULL` rather than emitting a partial transcription. Because `stop_name_r` is the single upstream source that fans out into the `stations` projection, `search_by_name`, and the romanized bus route/headsign names, this supplements every English-facing surface at once. When projecting into `stations`, `station_name_rn` is filled with the plain-ASCII spelling via `romaji::strip_macrons()` (Tōkyō → Tokyo), mirroring the rail dataset's `_r` (macron) / `_rn` (macron-free) column pair. -- **Track distances** – `Station.trackDistanceFromPrevious` is the track length in meters from the station before it in the returned list, for clients that total a ride's distance (TrainLCD/MobileApp's ride log). It is distinct from `trainRoute`'s `distanceFromPrevious`, which stays the straight-line distance the app's running simulation relies on. Only `lineStations`, `lineGroupStations`, `trainRoute`, and `stations(ids)` fill it (`attach_track_distances` in `QueryInteractor`, called once the order is final). `stations(ids)` returns the stations in the order of `ids` — the repository keeps that order and enrichment never reorders — so a client passing a route's station IDs (MobileApp's `sids` deep links) gets the lengths between IDs adjacent in that order; a pair that is not adjacent in the track data (an ID list skipping stations) is `null`. The first station, the first station of each `trainRoute` leg, sections without N02 geometry, and every other query return `null`, and clients fall back to the straight line there. `preprocessor/src/track/` builds an undirected graph from N02's `RailroadSection` LineStrings, collects every pair adjacent in a line's `(e_sort, station_cd)` order or a line group's `station_station_types.id` order (with and without closed stations, plus the seam of loop services within 3 km), snaps each station to every track within 500 m, and takes the Dijkstra path minimizing `2 × snap offset + track length` — snapping to the nearest track alone picks another line of the same operator at large stations (Honmachi) and detours through a transfer station. It first measures on the operators of both stations' companies (`OPERATOR_ALIASES` maps companies whose name differs from N02's), then retries on every operator for lines running on another company's track; a path longer than max(3 × straight, straight + 5 km) counts as unmeasured, and a result shorter than the straight line is raised to it. Pairs in one station group are 0. `build.rs` writes the sorted pairs to `connections.bin`, and `index::track_distance` binary-searches it, so no index is built at isolate start. When bumping the N02 edition, change the URL and cache directory in `preprocessor/src/track/mod.rs` and the `Cache N02 railway data` step in `.github/actions/build-worker/action.yml` together and compare the unmeasured count in the `preprocessor` log. `docs/architecture.md` (駅間の線路の長さ) has the details. +- **Track distances** – `Station.trackDistanceFromPrevious` is the track length in meters from the station before it in the returned list, for clients that total a ride's distance (TrainLCD/MobileApp's ride log). It is distinct from `trainRoute`'s `distanceFromPrevious`, which stays the straight-line distance the app's running simulation relies on. The arrival estimation (`estimateArrivalTimes`, `trainRoute` with `Estimated`, and the ride times of `connectedRoutes`) uses the track length as the running distance wherever one exists and falls back to straight line × detour factor elsewhere; `scripts/compute_speed_table.py` fits the speed tables with the same distances, so it needs `generated/` from `make data`. Only `lineStations`, `lineGroupStations`, `trainRoute`, and `stations(ids)` fill it (`attach_track_distances` in `QueryInteractor`, called once the order is final). `stations(ids)` returns the stations in the order of `ids` — the repository keeps that order and enrichment never reorders — so a client passing a route's station IDs (MobileApp's `sids` deep links) gets the lengths between IDs adjacent in that order; a pair that is not adjacent in the track data (an ID list skipping stations) is `null`. The first station, the first station of each `trainRoute` leg, sections without N02 geometry, and every other query return `null`, and clients fall back to the straight line there. `preprocessor/src/track/` builds an undirected graph from N02's `RailroadSection` LineStrings, collects every pair adjacent in a line's `(e_sort, station_cd)` order or a line group's `station_station_types.id` order (with and without closed stations, plus the seam of loop services within 3 km), snaps each station to every track within 500 m, and takes the Dijkstra path minimizing `2 × snap offset + track length` — snapping to the nearest track alone picks another line of the same operator at large stations (Honmachi) and detours through a transfer station. It first measures on the operators of both stations' companies (`OPERATOR_ALIASES` maps companies whose name differs from N02's), then retries on every operator for lines running on another company's track; a path longer than max(3 × straight, straight + 5 km) counts as unmeasured, and a result shorter than the straight line is raised to it. Pairs in one station group are 0. `build.rs` writes the sorted pairs to `connections.bin`, and `index::track_distance` binary-searches it, so no index is built at isolate start. When bumping the N02 edition, change the URL and cache directory in `preprocessor/src/track/mod.rs` and the `Cache N02 railway data` step in `.github/actions/build-worker/action.yml` together and compare the unmeasured count in the `preprocessor` log. `docs/architecture.md` (駅間の線路の長さ) has the details. - **TTS metadata** – `Station`, `StationNested`, `Line`, `LineNested`, `TrainType`, and `TrainTypeNested` expose `name_ipa` / `name_roman_ipa` plus `name_tts_segments` for multi-segment pronunciation output. Use `name_tts_segments` when clients need per-token SSML construction for mixed-language names such as `Kasai-Rinkai Park`. - **Connected routes** – `connectedRoutes` finds transfer routes automatically, like a journey planner, using a frequency-based RAPTOR search in `stationapi/src/domain/route_search.rs`. Each rail line group is a pattern, station groups are the transfer nodes, and ride times come from `arrival_estimation`; bus lines are excluded. The cost adds a per-boarding wait by `TrainTypeKind` (limited express 15 min, express / high-speed rapid 5 min, others 3 min) and a 3-minute transfer walk — without the wait, infrequent limited expresses would beat the Yamanote Line. Rounds give the time/transfer Pareto set; alternatives come from re-searching with one leg's parallel line groups banned along that leg (at most 8 searches), and are dropped beyond 1.15 × best + 15 min or with two more transfers than the Pareto set. Alternative routes that stop at the same station group in two different legs (backtracking to re-board a banned train) are dropped; pass-through stations are not counted, and the Pareto routes of the first search are never dropped this way (otherwise a station `stationsByName` reports as reachable could get no route). Results are ranked by cost + 5 min per transfer and capped at 6. The time and transfer count stay internal (the API does not return them), so ordering happens on the server: `sortBy: ConnectedRouteSort` picks `Recommended` (the ranking above, the default when omitted), `ArrivalTime` (the estimated time `estimateArrivalTimes` also reports — excluding the first train's wait — then fewer transfers), or `TransferCount` (fewer transfers, then earlier arrival). `route_search::sort_journeys` only reorders the set `search` returned, with a stable sort so ties keep the recommended order; the set itself never depends on `sortBy`. The network (every rail line group plus its time estimates, about 190 ms natively) is built lazily into a `OnceLock` by `StationRepository::get_route_network` on the first `connectedRoutes` call, so other queries never pay for it. Each route is a list of `legs` shaped for the app's one-train-at-a-time flow: every leg carries its boarding and alighting `Station`, both on the line of the line group the search rode — so at a transfer the previous leg's alighting station and the next leg's boarding station may be different stations of one station group — `stationGroupIds`, the station groups from boarding to alighting in travel order including pass-through stations (the search's `JourneyLeg.station_group_ids` as-is — station groups rather than station IDs so the client can match them against whichever train type it picks, which may run on another line; a group appears twice on patterns such as the Oedo Line's Tochomae), and `trainTypes`, every train type usable on that leg (real `groupId`s, so the client picks one and calls `lineGroupStations`): it is exactly `routeTypes(boarding station group, alighting station group, alighting station's line)` (same dedup, same `lines`, same order — the use case calls `get_train_types`), because the search collapses parallel services such as local and rapid into one route and the app needs them to list types and default to the local. `viaLineId`, like `routeTypes`, is the line of the tapped search result and keeps only routes whose last leg arrives on that line. `estimateArrivalTimes` and `trainRoute` accept `legs: [RouteLegInput!]` (the `groupId` of the train type picked from each leg's `trainTypes`, plus the leg's `fromStation.id` and `toStation.id`) and then return values for the whole transfer route. A leg endpoint missing from the chosen line group is matched by station group (the picked local may stop at another line's station of the same group), preferring an exact `station_cd` and, among same-group candidates, the pair giving the shortest slice (through services list two stations of a junction group). ETA estimates each leg on its own line group only and chains them from the origin, adding the 3-minute walk and the next train type's wait at each transfer (the same allowance the search ranks by), returning one route with an empty `id`; `trainRoute` concatenates each leg's segments (each leg restarts at distance 0). Both slice legs with the same function, taking the shorter arc on loop lines, so their station sequences match. More than `MAX_RIDES` (6) legs — more than `connectedRoutes` ever returns — legs that do not connect, ends that differ from `fromStationId` / `toStationId`, or combining `legs` with `viaLineIds` / `directionId` / `lineGroupId` are errors. `docs/architecture.md` (乗換経路探索) has the details, and `docs/route-search.md` documents the search internals (data structures, the scan, pruning, alternatives, determinism). - Changes to the published contract require coordinated updates to `schema/public.graphql`, the async-graphql types in `src/graphql/`, and, when the shape of a value changes, `stationapi/src/model.rs` and the DTO conversions. diff --git a/README.md b/README.md index fc3a17de..72a6527d 100644 --- a/README.md +++ b/README.md @@ -33,9 +33,10 @@ This project includes a comprehensive dataset of Japanese railway information in `preprocessor/src/gtfs/feed.rs` and `preprocessor/src/gtfs/odpt.rs`. - Some of the reference travel times in `travel_times/cases.csv` are derived from the Toei Subway GTFS (Bureau of Transportation, Tokyo Metropolitan Government, - CC BY 4.0) and from the Tokyo Metro and Metropolitan Intercity Railway - (Tsukuba Express) GTFS feeds published by the Public Transportation Open Data - Center under the Public Transportation Open Data Basic License. + CC BY 4.0) and from the Tokyo Metro, Metropolitan Intercity Railway (Tsukuba + Express), Tokyo Waterfront Area Rapid Transit (Rinkai Line), and Tama Toshi + Monorail GTFS feeds published by the Public Transportation Open Data Center + under the Public Transportation Open Data Basic License. ## Contributors ✨ diff --git a/docs/architecture.md b/docs/architecture.md index 9ff2fd9f..698fe482 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -481,7 +481,9 @@ input RouteLegInput { lineGroupId: Int! fromStationId: Int! toStationId: Int! - `Legacy` (省略時): 追加した時点 (#1568) のモデルです。最高速度と加減速は `dto::simulation::resolve_speed_profile` が決め、到着・出発の見込み (`arrivalCumulativeMinutes` / `departureCumulativeMinutes`) は `null` です。 - MobileApp のオートモードがこの値で走るので、値を変えません。 + MobileApp のオートモードがこの値で走るので、値を変えません。速度の較正は、 + 到着時間推定の較正を求め直す前の表を `domain/legacy_speed_table.rs` に凍結して + 使います。 - `Estimated`: 到着時間推定 (`arrival_estimation`) のモデルです。速度の較正 テーブルは、このモデルに当てはめて求めています。返す駅列に推定を掛け、停車・ 通過、最高速度、加減速を推定が使った値に置き換えて、到着・出発の見込みを入れ @@ -489,8 +491,10 @@ input RouteLegInput { lineGroupId: Int! fromStationId: Int! toStationId: Int! `estimateArrivalTimes` と同じ値です。バスの駅を含む経路は推定のモデルの対象外 なので、`Legacy` と同じ値を返します。 -2 つのモデルは、加減速、運転余裕率、停車時間、較正テーブルに無い路線・種別の -速度が違います。`Legacy` の値で台形の速度プロファイルを作って走らせると、 +2 つのモデルは、加減速、運転余裕率、停車時間、較正テーブル、駅間の距離が違い +ます。到着時間推定は、線路の長さ (`connections`) がある駅間ではそれを走行距離に +使い、無い駅間だけ直線距離 × 迂回係数で見積もります。乗換経路探索 +(`connectedRoutes`) の所要時間も同じ距離で求めます。`Legacy` の値で台形の速度プロファイルを作って走らせると、 `estimateArrivalTimes` より短い時間で走り切ります (#1709)。所要時間を推定に 合わせたいクライアントは、`Estimated` の見込みを使います。 @@ -760,6 +764,7 @@ repository の実装がないメソッドは、空の結果ではなく `DomainE │ │ ├── entity/ # Station / Line / TrainType / Company ... │ │ ├── repository/ # 抽象インターフェース │ │ ├── arrival_estimation.rs +│ │ ├── legacy_speed_table.rs # trainRoute の Legacy (オートモード) が使う凍結した較正 │ │ ├── route_search.rs # 乗換経路探索 (RAPTOR) │ │ ├── route_topology.rs # 所要時間を持たない系統網 (stationsByName の到達判定) │ │ ├── segment_speed_table.rs diff --git a/scripts/README.md b/scripts/README.md index c160efac..3b45211d 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -51,6 +51,11 @@ OSM データは [Open Database License (ODbL)](https://www.openstreetmap.org/co 2 種類を再計算します。運動学モデルを Python で再現し、実ダイヤの所要時間を 再現する実効最高速度を二分探索でフィッティングします。 +駅間の距離は、推定と同じく線路の長さ(`generated/connections.csv`)を使い、無い +駅間だけ直線距離 × 迂回係数で見積もります。先に `make data` で `generated/` を +作ってから実行してください。較正を変えたら、`make travel-time-report` で全体の +誤差が悪くならないことを確かめます(`travel_times/README.md`)。 + 1. **路線 × 列車種別**(`stationapi/src/domain/speed_table.rs`): 列車全体の所要時間へのフィット。一般則(路線種別の基本速度 × 種別倍率)から ±10% 以上乖離した路線だけを出力します。 diff --git a/scripts/compute_speed_table.py b/scripts/compute_speed_table.py index 4ae17365..b17e10dc 100644 --- a/scripts/compute_speed_table.py +++ b/scripts/compute_speed_table.py @@ -49,6 +49,10 @@ TYPES_CSV = os.path.join(ROOT, "data", "4!types.csv") SST_CSV = os.path.join(ROOT, "data", "5!station_station_types.csv") SPEED_TABLE_RS = os.path.join(ROOT, "stationapi", "src", "domain", "speed_table.rs") +# 隣り合う駅の線路の長さ。preprocessor (make data) が N02 から求め、Worker に埋め込む +# ものと同じ。到着時間推定はこれがある駅間では迂回係数ではなく線路の長さを使うので、 +# 較正も同じ距離で行う。 +CONNECTIONS_CSV = os.path.join(ROOT, "generated", "connections.csv") CACHE_DIR = os.path.join(HERE, ".gtfs_cache") HTTP_TIMEOUT = 120 @@ -71,7 +75,7 @@ class Feed: Feed( key="hakodate_tram", name="函館市電", - url="https://api-public.odpt.org/api/v4/files/odpt/HakodateCity/Alllines.zip?date=20260615", + url="https://api-public.odpt.org/api/v4/files/odpt/HakodateCity/Alllines.zip?date=20260815", needs_token=False, license="公共交通オープンデータセンター(認証なし公開)", ), @@ -309,6 +313,32 @@ def load_repo_data(): return lines, by_line, kinds, groups +def load_track_distances() -> dict[tuple[int, int], float]: + """隣り合う 2 駅の線路の長さ(m)。キーは (小さい station_cd, 大きい station_cd)。""" + if not os.path.isfile(CONNECTIONS_CSV): + raise SystemExit( + f"{CONNECTIONS_CSV} がありません。make data で generated/ を作ってから実行してください" + "(到着時間推定は線路の長さを使うので、較正も同じ距離で行う)" + ) + track: dict[tuple[int, int], float] = {} + with open(CONNECTIONS_CSV, newline="", encoding="utf-8") as f: + for r in csv.DictReader(f): + a, b = int(r["station_cd1"]), int(r["station_cd2"]) + d = float(r["distance"]) + if d > 0: + track[(min(a, b), max(a, b))] = d + return track + + +def pair_distance(track: dict, a: dict, b: dict, alpha: float) -> float: + """駅 a→b の走行距離(m)。estimate_arrival_minutes_with_track と同じく、線路の長さが + あればそれを、無ければ直線距離 × 迂回係数を使う。""" + d = track.get((min(a["cd"], b["cd"]), max(a["cd"], b["cd"]))) + if d: + return d + return haversine(a["lat"], a["lon"], b["lat"], b["lon"]) * alpha + + def line_detour(lines: dict, by_line: dict, line_cd: int) -> float: """路線全体で較正した迂回係数 α(estimate_arrival_minutes_calibrated と同等)。""" li = lines[line_cd] @@ -680,7 +710,7 @@ def segment_sanity_upper_kmh(base: float, line_type: int | None) -> float: def calibrate_feed( - feed: Feed, zf: zipfile.ZipFile, lines, by_line, kinds, groups + feed: Feed, zf: zipfile.ZipFile, lines, by_line, kinds, groups, track ) -> tuple[list[Calibration], dict[tuple[int, int, int], list[tuple[float, str]]]]: stops_by_id = { s["stop_id"]: (norm_name(s["stop_name"]), float(s["stop_lat"]), float(s["stop_lon"])) @@ -760,8 +790,7 @@ def calibrate_feed( alpha = line_detour(lines, by_line, line_cd) seq: list[tuple[float, bool]] = [(0.0, True)] for a, b in zip(span, span[1:]): - d = haversine(a["lat"], a["lon"], b["lat"], b["lon"]) * alpha - seq.append((d, b["cd"] in served)) + seq.append((pair_distance(track, a, b, alpha), b["cd"] in served)) obs = arr_last - dep0 dedup_key = (tuple(s["cd"] for s in span), tuple(sorted(served)), round(obs, 1)) @@ -989,6 +1018,7 @@ def main() -> int: token = os.environ.get("ODPT_ACCESS_TOKEN") lines, by_line, kinds, groups = load_repo_data() + track = load_track_distances() all_results: list[Calibration] = [] all_seg_samples: dict[tuple[int, int, int], list[tuple[float, str]]] = {} @@ -1005,7 +1035,7 @@ def main() -> int: if zf is None: continue with zf: - results, seg_samples = calibrate_feed(feed, zf, lines, by_line, kinds, groups) + results, seg_samples = calibrate_feed(feed, zf, lines, by_line, kinds, groups, track) all_results.extend(results) for key, ts in seg_samples.items(): all_seg_samples.setdefault(key, []).extend(ts) @@ -1043,9 +1073,10 @@ def line_baseline(line_cd: int) -> float: return general_rule_speed(lines[line_cd]["line_type"], 0) name_of = {s["cd"]: s["name"] for sts in by_line.values() for s in sts} - coord_of = {s["cd"]: (s["lat"], s["lon"]) for sts in by_line.values() for s in sts} + station_of = {s["cd"]: s for sts in by_line.values() for s in sts} - # ペアごとの平均ターゲットとみなし距離。距離には路線較正済みの迂回係数を使う。 + # ペアごとの平均ターゲットとみなし距離。距離は線路の長さ (無ければ路線較正済みの + # 迂回係数で見積もった直線距離) を使う。 mean_targets: dict[tuple[int, int, int], float] = {} pair_dist_m: dict[tuple[int, int, int], float] = {} # 出発間隔ベースでしか較正できなかったペア(繰り越し補正の対象)。 @@ -1056,7 +1087,6 @@ def line_baseline(line_cd: int) -> float: continue if line_cd not in detour_cache: detour_cache[line_cd] = line_detour(lines, by_line, line_cd) - (lat1, lon1), (lat2, lon2) = coord_of[cd_lo], coord_of[cd_hi] key = (line_cd, cd_lo, cd_hi) # 到着時刻ベース(純走行時間)が十分あればそれだけを使う。無いフィード # (東京メトロは全駅 arr==dep)では出発間隔ベースの上側トリム平均で @@ -1070,7 +1100,9 @@ def line_baseline(line_cd: int) -> float: used = dep_ts[:keep] dep_only_keys.add(key) mean_targets[key] = sum(used) / len(used) - pair_dist_m[key] = haversine(lat1, lon1, lat2, lon2) * detour_cache[line_cd] + pair_dist_m[key] = pair_distance( + track, station_of[cd_lo], station_of[cd_hi], detour_cache[line_cd] + ) raw_targets = dict(mean_targets) rebalance_line_targets(mean_targets, pair_dist_m, by_line, lines, dep_only_keys) diff --git a/src/repository.rs b/src/repository.rs index 945c8bf9..2f82aa0c 100644 --- a/src/repository.rs +++ b/src/repository.rs @@ -158,9 +158,10 @@ fn route_network() -> &'static Arc { } fn build_route_network() -> RouteNetwork { - RouteNetwork::build( + RouteNetwork::build_with_track( rail_line_group_cds().map(|group| stations_of_line_groups(&[group as u32])), &EstimationParams::default(), + index::track_distance, ) } diff --git a/stationapi/src/domain.rs b/stationapi/src/domain.rs index 25ffc7f9..5bb535e7 100644 --- a/stationapi/src/domain.rs +++ b/stationapi/src/domain.rs @@ -2,6 +2,7 @@ pub mod arrival_estimation; pub mod entity; pub mod error; pub mod ipa; +pub mod legacy_speed_table; pub mod normalize; pub mod repository; pub mod romaji; diff --git a/stationapi/src/domain/arrival_estimation.rs b/stationapi/src/domain/arrival_estimation.rs index 258cb2a5..087f1905 100644 --- a/stationapi/src/domain/arrival_estimation.rs +++ b/stationapi/src/domain/arrival_estimation.rs @@ -538,6 +538,14 @@ pub fn estimate_arrival_minutes( estimate_arrival_minutes_calibrated(stops, stops, params) } +/// 隣り合う 2 駅の線路の長さ (m)。キーは `(小さい station_cd, 大きい station_cd)`。 +pub type TrackDistances = HashMap<(i32, i32), f64>; + +/// [`TrackDistances`] のキー。向きを問わずに引けるよう小さい駅を先にする。 +pub fn track_distance_key(a: i32, b: i32) -> (i32, i32) { + (a.min(b), a.max(b)) +} + /// [`estimate_arrival_minutes`] の較正母数指定版。 /// /// `calibration_stops` には経路全体(部分区間へ切り出す前)の駅列を渡す。 @@ -549,6 +557,19 @@ pub fn estimate_arrival_minutes_calibrated( stops: &[&Station], calibration_stops: &[&Station], params: &EstimationParams, +) -> Vec { + estimate_arrival_minutes_with_track(stops, calibration_stops, params, None) +} + +/// [`estimate_arrival_minutes_calibrated`] に、駅間の線路の長さを渡せる版。 +/// +/// `track` に隣り合う 2 駅の線路の長さがあれば、その駅間の走行距離は直線距離 × +/// 迂回係数ではなく線路の長さを使う。無い駅間は従来どおり迂回係数で見積もる。 +pub fn estimate_arrival_minutes_with_track( + stops: &[&Station], + calibration_stops: &[&Station], + params: &EstimationParams, + track: Option<&TrackDistances>, ) -> Vec { let n = stops.len(); if n == 0 { @@ -676,7 +697,16 @@ pub fn estimate_arrival_minutes_calibrated( let mut seg: Vec<(f64, f64, usize, bool)> = Vec::new(); for i in 1..n { - let track_m = straight_km[i] * detour_of(stops[i]) * 1000.0; + let track_m = track + .and_then(|t| { + t.get(&track_distance_key( + stops[i - 1].station_cd, + stops[i].station_cd, + )) + }) + .copied() + .filter(|d| *d > 0.0) + .unwrap_or_else(|| straight_km[i] * detour_of(stops[i]) * 1000.0); let v_kmh = speed_kmh_at(i); let idx = result.len(); @@ -1139,38 +1169,6 @@ mod tests { } } - #[test] - fn ginza_line_shibuya_to_shimbashi_matches_real_travel_time() { - let p = EstimationParams::default(); - // 東京メトロ銀座線 渋谷→新橋。実座標・実データ値 - // (average_distance = 780.46154m、地下鉄 line_type=3)。 - // 実乗車時間は東京メトロ/乗換案内系の標準所要時間で約13分。 - let data = [ - (2800119, 35.659066, 139.701000), // 渋谷 - (2800118, 35.665247, 139.712314), // 表参道 - (2800117, 35.670527, 139.717857), // 外苑前 - (2800116, 35.672765, 139.724159), // 青山一丁目 - (2800115, 35.677021, 139.737047), // 赤坂見附 - (2800114, 35.673621, 139.741419), // 溜池山王 - (2800113, 35.670236, 139.749832), // 虎ノ門 - (2800112, 35.667434, 139.758432), // 新橋 - ]; - let stations: Vec = data - .iter() - .map(|&(cd, lat, lon)| { - let mut s = station(cd, 28001, lat, lon, Some(780.46154)); - s.line_type = Some(LINE_TYPE_SUBWAY); - s.line_group_cd = Some(28001); - s - }) - .collect(); - let refs: Vec<&Station> = stations.iter().collect(); - let est = estimate_arrival_minutes(&refs, &p); - - let total = est.last().unwrap().cumulative_minutes; - assert!((12.0..14.0).contains(&total), "got {total}"); - } - /// GTFS インポート後のバス停を再現する(line_type=3 は GTFS route_type のバス、 /// kind=7 は TrainTypeKind::BusRoute、average_distance は無し)。 fn bus_station(station_cd: i32, line_cd: i32, lat: f64, lon: f64) -> Station { @@ -1456,8 +1454,8 @@ mod tests { #[test] fn speed_table_override_beats_general_rule() { - // 京急本線(27001)の快特(Express)は較正テーブルの 120km/h が適用され、 - // 一般則(80×1.15=92km/h)の路線より同一区間を速く走る。 + // 阪急神戸本線(34001)の特急(LimitedExpress)は較正テーブルの 110km/h が + // 適用され、一般則(80×1.2=96km/h)の路線より同一区間を速く走る。 let p = EstimationParams::default(); let time_on_line = |line_cd: i32| -> f64 { // 8km 区間(巡航支配)で比較する。0.072 度 ≈ 8km。 @@ -1472,21 +1470,21 @@ mod tests { ]; stations[2].pass = Some(1); for s in stations.iter_mut() { - s.kind = Some(TrainTypeKind::Express as i32); + s.kind = Some(TrainTypeKind::LimitedExpress as i32); } let refs: Vec<&Station> = stations.iter().collect(); estimate_arrival_minutes(&refs, &p)[1].cumulative_minutes }; - let keikyu = time_on_line(27001); + let hankyu = time_on_line(34001); let generic = time_on_line(100); assert!( - keikyu < generic, - "keikyu {keikyu} should be < generic {generic}" + hankyu < generic, + "hankyu {hankyu} should be < generic {generic}" ); - // テーブル値 120km/h での運動学モデルと厳密に一致する。 + // テーブル値 110km/h での運動学モデルと厳密に一致する。 let straight_m = haversine_distance(35.000, 139.0, 35.072, 139.0); // average_distance 無し → 在来線フォールバック α=1.30。 - approx(keikyu, segment_run_minutes(straight_m * 1.30, 120.0, &p)); + approx(hankyu, segment_run_minutes(straight_m * 1.30, 110.0, &p)); } #[test] @@ -1673,4 +1671,43 @@ mod tests { segment_run_minutes(track_m, est[1].max_speed_kmh, &p), ); } + + #[test] + fn track_distance_replaces_the_detour_estimate_where_known() { + let p = EstimationParams::default(); + let stations = three_collinear_stations(); + let refs: Vec<&Station> = stations.iter().collect(); + let without = estimate_arrival_minutes(&refs, &p); + + // 1→2 だけ線路の長さ (直線の 2 倍) を渡す。2→3 は迂回係数のまま + let straight_m = haversine_distance(35.000, 139.0, 35.016, 139.0); + let mut track = TrackDistances::new(); + track.insert(track_distance_key(2, 1), straight_m * 2.0); + let with = estimate_arrival_minutes_with_track(&refs, &refs, &p, Some(&track)); + + approx( + with[1].cumulative_minutes, + segment_run_minutes(straight_m * 2.0, with[1].max_speed_kmh, &p), + ); + // 線路の長さが無い駅間の走行時間は変わらない + approx( + with[2].cumulative_minutes - with[1].departure_cumulative_minutes, + without[2].cumulative_minutes - without[1].departure_cumulative_minutes, + ); + // 表を渡さない (None) のは従来どおり + let none = estimate_arrival_minutes_with_track(&refs, &refs, &p, None); + approx(none[2].cumulative_minutes, without[2].cumulative_minutes); + } + + #[test] + fn non_positive_track_distance_falls_back_to_the_detour_estimate() { + let p = EstimationParams::default(); + let stations = three_collinear_stations(); + let refs: Vec<&Station> = stations.iter().collect(); + let without = estimate_arrival_minutes(&refs, &p); + let mut track = TrackDistances::new(); + track.insert(track_distance_key(1, 2), 0.0); + let with = estimate_arrival_minutes_with_track(&refs, &refs, &p, Some(&track)); + approx(with[1].cumulative_minutes, without[1].cumulative_minutes); + } } diff --git a/stationapi/src/domain/legacy_speed_table.rs b/stationapi/src/domain/legacy_speed_table.rs new file mode 100644 index 00000000..c3e395c0 --- /dev/null +++ b/stationapi/src/domain/legacy_speed_table.rs @@ -0,0 +1,934 @@ +//! `trainRoute` の `Legacy` モデル (MobileApp のオートモード) が使う速度の較正テーブル。 +//! +//! 到着時間推定の較正 (`speed_table` / `segment_speed_table`) は、距離の出し方を +//! 線路の長さへ替えたときに求め直した。`Legacy` はオートモードの走り方を変えない +//! ために、求め直す前の表 (TrainLCD/StationAPI の `dev`、#1711 の時点) をここに +//! 凍結して持つ。`scripts/compute_speed_table.py` はこのファイルを書き換えない。 +//! 値を変えるとオートモードの速度が変わるので、変えないこと。 + +use crate::domain::segment_speed_table::segment_override_applies_to_kind; +use crate::model::TrainTypeKind; + +/// 手動較正 (凍結時点の `speed_table::LINE_SPEED_OVERRIDES`)。 +const LEGACY_LINE_SPEED_OVERRIDES: &[(i32, TrainTypeKind, f64)] = &[ + // 京急本線: 快特・特急 120km/h 運転。品川→横浜 快特 実17分。 + (27001, TrainTypeKind::Express, 120.0), + (27001, TrainTypeKind::LimitedExpress, 120.0), + // 小田急線: 最高 110km/h。新宿→町田 快速急行 実34分。 + (25001, TrainTypeKind::Express, 110.0), + // 東急東横線: 特急は最高 110km/h だが過密ダイヤ・急曲線で実効は各停並み。 + // 渋谷→横浜 特急 実27分。 + (26001, TrainTypeKind::LimitedExpress, 80.0), + // 京王井の頭線: 急行の実効速度。渋谷→吉祥寺 急行 実17分。 + (24006, TrainTypeKind::Express, 80.0), + // 京成本線(都心側): スカイライナーの上野→高砂間の実効速度。 + (23001, TrainTypeKind::LimitedExpress, 100.0), + // 成田スカイアクセス線: スカイライナー 160km/h 運転、アクセス特急 120km/h 運転。 + // 日暮里→空港第2ビル スカイライナー 実36分。 + (23006, TrainTypeKind::LimitedExpress, 160.0), + (23006, TrainTypeKind::Express, 120.0), + // つくばエクスプレス: 各停も 125km/h 運転(駅間が短いため実効値で較正)。 + // 秋葉原→つくば 普通 実66分。快速(kind=HighSpeedRapid)は一般則 120km/h で十分。 + (99309, TrainTypeKind::Default, 95.0), + // 阪急神戸本線: 特急 115km/h 運転。大阪梅田→神戸三宮 実27分。 + (34001, TrainTypeKind::LimitedExpress, 110.0), + // 近鉄特急(名阪甲特急ひのとり): 大阪難波→近鉄名古屋 実125分。 + // 難波線は地下線、大阪線は山間曲線区間を含むため実効値で較正。 + (31001, TrainTypeKind::LimitedExpress, 80.0), + (31005, TrainTypeKind::LimitedExpress, 115.0), + (31027, TrainTypeKind::LimitedExpress, 120.0), + // 都営大江戸線: リニア地下鉄で公表最高速度 70km/h(地下鉄一般則 75km/h より低い)。 + // 都営 GTFS の一様フィットは 60km/h だが、サンプルの大半が 6の字全区間直通 + // (光が丘⇔都庁前 環状部経由・実83〜88分)で、急曲線の多い環状部に引きずられ + // 放射部(落合南長崎→光が丘 実11分)が +14% になるため採用しない。 + // 公表最高速度 70km/h なら全ベンチマーク(放射部 実11分・新宿→光が丘 実24分・ + // 光が丘→都庁前 実22分・清澄白河→赤羽橋 実16分・全区間 実85分)が ±9% 以内。 + // 環状部南側(月島〜赤羽橋の河川横断・急曲線区間)の残差 -8% は路線一様速度では + // 解消できない(区間別較正が必要)。 + (99301, TrainTypeKind::Default, 70.0), +]; + +/// GTFS 自動較正 (凍結時点の `speed_table::LINE_SPEED_OVERRIDES_GTFS`)。 +const LEGACY_LINE_SPEED_OVERRIDES_GTFS: &[(i32, TrainTypeKind, f64)] = &[ + // 東京メトロ銀座線 Default: 東京メトロ GTFS 15本 中央値34分 (一般則 75km/h) + (28001, TrainTypeKind::Default, 55.0), + // 東京メトロ丸ノ内線 Default: 東京メトロ GTFS 10本 中央値51分 (一般則 75km/h) + (28002, TrainTypeKind::Default, 65.0), + // 東京メトロ日比谷線 Default: 東京メトロ GTFS 7本 中央値45分 (一般則 75km/h) + (28003, TrainTypeKind::Default, 50.0), + // 東京メトロ東西線 Default: 東京メトロ GTFS 20本 中央値54分 (一般則 75km/h) + (28004, TrainTypeKind::Default, 60.0), + // 東京メトロ有楽町線 Default: 東京メトロ GTFS 19本 中央値52分 (一般則 75km/h) + (28006, TrainTypeKind::Default, 65.0), + // 東京メトロ半蔵門線 Default: 東京メトロ GTFS 10本 中央値32分 (一般則 75km/h) + (28008, TrainTypeKind::Default, 55.0), + // 東京メトロ南北線 Default: 東京メトロ GTFS 12本 中央値38分 (一般則 75km/h) + (28009, TrainTypeKind::Default, 65.0), + // 東京メトロ副都心線 Default: 東京メトロ GTFS 31本 中央値27分 (一般則 75km/h) + (28010, TrainTypeKind::Default, 55.0), + // 東京メトロ副都心線 Express: 東京メトロ GTFS 15本 中央値28分 (一般則 86km/h) + (28010, TrainTypeKind::Express, 60.0), + // 函館市電2系統 Default: 函館市電 GTFS 4本 中央値48分 (一般則 40km/h) + (99105, TrainTypeKind::Default, 20.0), + // 函館市電5系統 Default: 函館市電 GTFS 6本 中央値47分 (一般則 40km/h) + (99106, TrainTypeKind::Default, 25.0), + // 都営浅草線 Default: 都営地下鉄 GTFS 18本 中央値37分 (一般則 75km/h) + (99302, TrainTypeKind::Default, 65.0), + // 都営浅草線 LimitedExpress: 都営地下鉄 GTFS 5本 中央値21分 (一般則 90km/h) + (99302, TrainTypeKind::LimitedExpress, 55.0), + // 東京さくらトラム(都電荒川線) Default: 都営地下鉄 GTFS 13本 中央値56分 (一般則 40km/h) + (99305, TrainTypeKind::Default, 25.0), + // 横浜市営地下鉄ブルーライン Rapid: 横浜市営地下鉄 GTFS 3本 中央値61分 (一般則 75km/h) + (99316, TrainTypeKind::Rapid, 65.0), + // 多摩モノレール Default: 多摩都市モノレール GTFS 6本 中央値38分 (一般則 60km/h) + (99334, TrainTypeKind::Default, 50.0), + // りんかい線 Default: りんかい線 GTFS 8本 中央値20分 (一般則 80km/h) + (99337, TrainTypeKind::Default, 65.0), + // 京都市営地下鉄東西線 Default: 京都市営地下鉄 GTFS 9本 中央値33分 (一般則 75km/h) + (99611, TrainTypeKind::Default, 65.0), +]; + +/// 駅間別較正 (凍結時点の `segment_speed_table::SEGMENT_SPEED_OVERRIDES`)。 +/// `(line_cd, station_cd_小, station_cd_大)` の昇順。 +const LEGACY_SEGMENT_SPEED_OVERRIDES: &[(i32, i32, i32, f64)] = &[ + // 東京メトロ銀座線 浅草〜田原町: 東京メトロ GTFS 218本 平均1.5分 (路線値 55km/h) + (28001, 2800101, 2800102, 34.0), + // 東京メトロ銀座線 田原町〜稲荷町: 東京メトロ GTFS 218本 平均1.2分 (路線値 55km/h) + (28001, 2800102, 2800103, 74.0), + // 東京メトロ銀座線 稲荷町〜上野: 東京メトロ GTFS 218本 平均1.3分 (路線値 55km/h) + (28001, 2800103, 2800104, 31.0), + // 東京メトロ銀座線 上野〜上野広小路: 東京メトロ GTFS 224本 平均1.4分 (路線値 55km/h) + (28001, 2800104, 2800105, 36.0), + // 東京メトロ銀座線 上野広小路〜末広町: 東京メトロ GTFS 224本 平均1.0分 (路線値 55km/h) + (28001, 2800105, 2800106, 75.0), + // 東京メトロ銀座線 末広町〜神田: 東京メトロ GTFS 224本 平均1.4分 (路線値 55km/h) + (28001, 2800106, 2800107, 105.0), + // 東京メトロ銀座線 神田〜三越前: 東京メトロ GTFS 224本 平均1.2分 (路線値 55km/h) + (28001, 2800107, 2800108, 90.0), + // 東京メトロ銀座線 三越前〜日本橋: 東京メトロ GTFS 224本 平均1.0分 (路線値 55km/h) + (28001, 2800108, 2800109, 77.0), + // 東京メトロ銀座線 日本橋〜京橋: 東京メトロ GTFS 224本 平均1.1分 (路線値 55km/h) + (28001, 2800109, 2800110, 83.0), + // 東京メトロ銀座線 京橋〜銀座: 東京メトロ GTFS 224本 平均1.2分 (路線値 55km/h) + (28001, 2800110, 2800111, 90.0), + // 東京メトロ銀座線 銀座〜新橋: 東京メトロ GTFS 224本 平均1.2分 (路線値 55km/h) + (28001, 2800111, 2800112, 87.0), + // 東京メトロ銀座線 新橋〜虎ノ門: 東京メトロ GTFS 224本 平均1.3分 (路線値 55km/h) + (28001, 2800112, 2800113, 94.0), + // 東京メトロ銀座線 虎ノ門〜溜池山王: 東京メトロ GTFS 224本 平均1.3分 (路線値 55km/h) + (28001, 2800113, 2800114, 95.0), + // 東京メトロ銀座線 溜池山王〜赤坂見附: 東京メトロ GTFS 224本 平均1.0分 (路線値 55km/h) + (28001, 2800114, 2800115, 76.0), + // 東京メトロ銀座線 赤坂見附〜青山一丁目: 東京メトロ GTFS 224本 平均2.2分 (路線値 55km/h) + (28001, 2800115, 2800116, 48.0), + // 東京メトロ銀座線 青山一丁目〜外苑前: 東京メトロ GTFS 224本 平均1.1分 (路線値 55km/h) + (28001, 2800116, 2800117, 81.0), + // 東京メトロ銀座線 外苑前〜表参道: 東京メトロ GTFS 224本 平均1.2分 (路線値 55km/h) + (28001, 2800117, 2800118, 90.0), + // 東京メトロ銀座線 表参道〜渋谷: 東京メトロ GTFS 224本 平均1.6分 (路線値 55km/h) + (28001, 2800118, 2800119, 94.0), + // 東京メトロ丸ノ内線 茗荷谷〜後楽園: 東京メトロ GTFS 149本 平均2.2分 (路線値 65km/h) + (28002, 2800203, 2800204, 74.0), + // 東京メトロ丸ノ内線 後楽園〜本郷三丁目: 東京メトロ GTFS 149本 平均1.2分 (路線値 65km/h) + (28002, 2800204, 2800205, 91.0), + // 東京メトロ丸ノ内線 本郷三丁目〜御茶ノ水: 東京メトロ GTFS 149本 平均1.3分 (路線値 65km/h) + (28002, 2800205, 2800206, 93.0), + // 東京メトロ丸ノ内線 御茶ノ水〜淡路町: 東京メトロ GTFS 149本 平均1.2分 (路線値 65km/h) + (28002, 2800206, 2800207, 87.0), + // 東京メトロ丸ノ内線 淡路町〜大手町: 東京メトロ GTFS 149本 平均1.4分 (路線値 65km/h) + (28002, 2800207, 2800208, 105.0), + // 東京メトロ丸ノ内線 大手町〜東京: 東京メトロ GTFS 149本 平均1.1分 (路線値 65km/h) + (28002, 2800208, 2800209, 79.0), + // 東京メトロ丸ノ内線 東京〜銀座: 東京メトロ GTFS 149本 平均2.2分 (路線値 65km/h) + (28002, 2800209, 2800210, 44.0), + // 東京メトロ丸ノ内線 霞ケ関〜国会議事堂前: 東京メトロ GTFS 149本 平均1.0分 (路線値 65km/h) + (28002, 2800211, 2800212, 76.0), + // 東京メトロ丸ノ内線 国会議事堂前〜赤坂見附: 東京メトロ GTFS 149本 平均1.3分 (路線値 65km/h) + (28002, 2800212, 2800213, 75.0), + // 東京メトロ丸ノ内線 赤坂見附〜四ツ谷: 東京メトロ GTFS 149本 平均2.3分 (路線値 65km/h) + (28002, 2800213, 2800214, 40.0), + // 東京メトロ丸ノ内線 四ツ谷〜四谷三丁目: 東京メトロ GTFS 149本 平均1.4分 (路線値 65km/h) + (28002, 2800214, 2800215, 104.0), + // 東京メトロ丸ノ内線 四谷三丁目〜新宿御苑前: 東京メトロ GTFS 149本 平均1.3分 (路線値 65km/h) + (28002, 2800215, 2800216, 82.0), + // 東京メトロ丸ノ内線 新宿御苑前〜新宿三丁目: 東京メトロ GTFS 149本 平均1.1分 (路線値 65km/h) + (28002, 2800216, 2800217, 81.0), + // 東京メトロ丸ノ内線 新宿〜西新宿: 東京メトロ GTFS 149本 平均1.2分 (路線値 65km/h) + (28002, 2800218, 2800219, 92.0), + // 東京メトロ丸ノ内線 西新宿〜中野坂上: 東京メトロ GTFS 149本 平均1.4分 (路線値 65km/h) + (28002, 2800219, 2800220, 105.0), + // 東京メトロ丸ノ内線 中野坂上〜新中野: 東京メトロ GTFS 149本 平均1.7分 (路線値 65km/h) + (28002, 2800220, 2800221, 83.0), + // 東京メトロ丸ノ内線 新中野〜東高円寺: 東京メトロ GTFS 149本 平均1.4分 (路線値 65km/h) + (28002, 2800221, 2800222, 107.0), + // 東京メトロ丸ノ内線 東高円寺〜新高円寺: 東京メトロ GTFS 149本 平均1.3分 (路線値 65km/h) + (28002, 2800222, 2800223, 99.0), + // 東京メトロ丸ノ内線 南阿佐ケ谷〜荻窪: 東京メトロ GTFS 149本 平均2.0分 (路線値 65km/h) + (28002, 2800224, 2800225, 76.0), + // 東京メトロ日比谷線 北千住〜南千住: 東京メトロ GTFS 177本 平均2.3分 (路線値 50km/h) + (28003, 2800301, 2800302, 68.0), + // 東京メトロ日比谷線 入谷〜上野: 東京メトロ GTFS 184本 平均1.5分 (路線値 50km/h) + (28003, 2800304, 2800305, 109.0), + // 東京メトロ日比谷線 上野〜仲御徒町: 東京メトロ GTFS 184本 平均1.0分 (路線値 50km/h) + (28003, 2800305, 2800306, 76.0), + // 東京メトロ日比谷線 仲御徒町〜秋葉原: 東京メトロ GTFS 184本 平均1.4分 (路線値 50km/h) + (28003, 2800306, 2800307, 100.0), + // 東京メトロ日比谷線 秋葉原〜小伝馬町: 東京メトロ GTFS 184本 平均1.3分 (路線値 50km/h) + (28003, 2800307, 2800308, 96.0), + // 東京メトロ日比谷線 小伝馬町〜人形町: 東京メトロ GTFS 184本 平均1.1分 (路線値 50km/h) + (28003, 2800308, 2800309, 80.0), + // 東京メトロ日比谷線 人形町〜茅場町: 東京メトロ GTFS 184本 平均1.2分 (路線値 50km/h) + (28003, 2800309, 2800310, 90.0), + // 東京メトロ日比谷線 茅場町〜八丁堀: 東京メトロ GTFS 184本 平均1.1分 (路線値 50km/h) + (28003, 2800310, 2800311, 80.0), + // 東京メトロ日比谷線 八丁堀〜築地: 東京メトロ GTFS 184本 平均1.3分 (路線値 50km/h) + (28003, 2800311, 2800312, 95.0), + // 東京メトロ日比谷線 築地〜東銀座: 東京メトロ GTFS 184本 平均1.0分 (路線値 50km/h) + (28003, 2800312, 2800313, 73.0), + // 東京メトロ日比谷線 東銀座〜銀座: 東京メトロ GTFS 184本 平均1.1分 (路線値 50km/h) + (28003, 2800313, 2800314, 35.0), + // 東京メトロ日比谷線 銀座〜日比谷: 東京メトロ GTFS 184本 平均0.9分 (路線値 50km/h) + (28003, 2800314, 2800315, 69.0), + // 東京メトロ日比谷線 日比谷〜霞ケ関: 東京メトロ GTFS 184本 平均1.7分 (路線値 50km/h) + (28003, 2800315, 2800316, 43.0), + // 東京メトロ日比谷線 霞ケ関〜虎ノ門ヒルズ: 東京メトロ GTFS 184本 平均1.2分 (路線値 50km/h) + (28003, 2800316, 2800322, 90.0), + // 東京メトロ日比谷線 神谷町〜六本木: 東京メトロ GTFS 184本 平均1.6分 (路線値 50km/h) + (28003, 2800317, 2800318, 83.0), + // 東京メトロ日比谷線 神谷町〜虎ノ門ヒルズ: 東京メトロ GTFS 184本 平均1.0分 (路線値 50km/h) + (28003, 2800317, 2800322, 77.0), + // 東京メトロ日比谷線 広尾〜恵比寿: 東京メトロ GTFS 184本 平均2.4分 (路線値 50km/h) + (28003, 2800319, 2800320, 45.0), + // 東京メトロ日比谷線 恵比寿〜中目黒: 東京メトロ GTFS 184本 平均2.0分 (路線値 50km/h) + (28003, 2800320, 2800321, 43.0), + // 東京メトロ東西線 中野〜落合: 東京メトロ GTFS 126本 平均2.5分 (路線値 60km/h) + (28004, 2800401, 2800402, 71.0), + // 東京メトロ東西線 落合〜高田馬場: 東京メトロ GTFS 126本 平均2.5分 (路線値 60km/h) + (28004, 2800402, 2800403, 51.0), + // 東京メトロ東西線 早稲田〜神楽坂: 東京メトロ GTFS 126本 平均1.5分 (路線値 60km/h) + (28004, 2800404, 2800405, 95.0), + // 東京メトロ東西線 神楽坂〜飯田橋: 東京メトロ GTFS 126本 平均2.1分 (路線値 60km/h) + (28004, 2800405, 2800406, 41.0), + // 東京メトロ東西線 飯田橋〜九段下: 東京メトロ GTFS 126本 平均1.4分 (路線値 60km/h) + (28004, 2800406, 2800407, 68.0), + // 東京メトロ東西線 九段下〜竹橋: 東京メトロ GTFS 126本 平均1.4分 (路線値 60km/h) + (28004, 2800407, 2800408, 45.0), + // 東京メトロ東西線 竹橋〜大手町: 東京メトロ GTFS 126本 平均1.4分 (路線値 60km/h) + (28004, 2800408, 2800409, 105.0), + // 東京メトロ東西線 大手町〜日本橋: 東京メトロ GTFS 126本 平均1.3分 (路線値 60km/h) + (28004, 2800409, 2800410, 57.0), + // 東京メトロ東西線 日本橋〜茅場町: 東京メトロ GTFS 126本 平均1.1分 (路線値 60km/h) + (28004, 2800410, 2800411, 81.0), + // 東京メトロ東西線 茅場町〜門前仲町: 東京メトロ GTFS 126本 平均2.0分 (路線値 60km/h) + (28004, 2800411, 2800412, 82.0), + // 東京メトロ東西線 門前仲町〜木場: 東京メトロ GTFS 126本 平均1.4分 (路線値 60km/h) + (28004, 2800412, 2800413, 103.0), + // 東京メトロ東西線 木場〜東陽町: 東京メトロ GTFS 126本 平均1.3分 (路線値 60km/h) + (28004, 2800413, 2800414, 99.0), + // 東京メトロ東西線 南砂町〜西葛西: 東京メトロ GTFS 117本 平均2.4分 (路線値 60km/h) + (28004, 2800415, 2800416, 106.0), + // 東京メトロ東西線 西葛西〜葛西: 東京メトロ GTFS 117本 平均1.6分 (路線値 60km/h) + (28004, 2800416, 2800417, 81.0), + // 東京メトロ東西線 葛西〜浦安: 東京メトロ GTFS 117本 平均2.5分 (路線値 60km/h) + (28004, 2800417, 2800418, 66.0), + // 東京メトロ東西線 浦安〜南行徳: 東京メトロ GTFS 117本 平均1.5分 (路線値 60km/h) + (28004, 2800418, 2800419, 107.0), + // 東京メトロ東西線 行徳〜妙典: 東京メトロ GTFS 117本 平均1.6分 (路線値 60km/h) + (28004, 2800420, 2800421, 90.0), + // 東京メトロ東西線 妙典〜原木中山: 東京メトロ GTFS 113本 平均2.1分 (路線値 60km/h) + (28004, 2800421, 2800422, 108.0), + // 東京メトロ東西線 原木中山〜西船橋: 東京メトロ GTFS 113本 平均1.9分 (路線値 60km/h) + (28004, 2800422, 2800423, 84.0), + // 東京メトロ千代田線 北綾瀬〜綾瀬: 東京メトロ GTFS 50本 平均4.2分 (路線値 75km/h) + (28005, 2800501, 2800502, 33.0), + // 東京メトロ千代田線 綾瀬〜北千住: 東京メトロ GTFS 176本 平均3.2分 (路線値 75km/h) + (28005, 2800502, 2800503, 66.0), + // 東京メトロ千代田線 北千住〜町屋: 東京メトロ GTFS 176本 平均2.7分 (路線値 75km/h) + (28005, 2800503, 2800504, 79.0), + // 東京メトロ千代田線 西日暮里〜千駄木: 東京メトロ GTFS 176本 平均1.3分 (路線値 75km/h) + (28005, 2800505, 2800506, 95.0), + // 東京メトロ千代田線 千駄木〜根津: 東京メトロ GTFS 176本 平均1.4分 (路線値 75km/h) + (28005, 2800506, 2800507, 102.0), + // 東京メトロ千代田線 根津〜湯島: 東京メトロ GTFS 176本 平均1.5分 (路線値 75km/h) + (28005, 2800507, 2800508, 111.0), + // 東京メトロ千代田線 湯島〜新御茶ノ水: 東京メトロ GTFS 176本 平均1.6分 (路線値 75km/h) + (28005, 2800508, 2800509, 115.0), + // 東京メトロ千代田線 大手町〜二重橋前: 東京メトロ GTFS 176本 平均1.1分 (路線値 75km/h) + (28005, 2800510, 2800511, 81.0), + // 東京メトロ千代田線 二重橋前〜日比谷: 東京メトロ GTFS 176本 平均1.3分 (路線値 75km/h) + (28005, 2800511, 2800512, 96.0), + // 東京メトロ千代田線 日比谷〜霞ケ関: 東京メトロ GTFS 176本 平均1.2分 (路線値 75km/h) + (28005, 2800512, 2800513, 89.0), + // 東京メトロ千代田線 国会議事堂前〜赤坂: 東京メトロ GTFS 176本 平均1.1分 (路線値 75km/h) + (28005, 2800514, 2800515, 84.0), + // 東京メトロ千代田線 乃木坂〜表参道: 東京メトロ GTFS 176本 平均1.6分 (路線値 75km/h) + (28005, 2800516, 2800517, 119.0), + // 東京メトロ千代田線 表参道〜明治神宮前〈原宿〉: 東京メトロ GTFS 176本 平均1.3分 (路線値 75km/h) + (28005, 2800517, 2800518, 98.0), + // 東京メトロ千代田線 明治神宮前〈原宿〉〜代々木公園: 東京メトロ GTFS 176本 平均1.8分 (路線値 75km/h) + (28005, 2800518, 2800519, 84.0), + // 東京メトロ千代田線 代々木公園〜代々木上原: 東京メトロ GTFS 176本 平均1.3分 (路線値 75km/h) + (28005, 2800519, 2800520, 95.0), + // 東京メトロ有楽町線 和光市〜地下鉄成増: 東京メトロ GTFS 90本 平均2.5分 (路線値 65km/h) + (28006, 2800601, 2800602, 79.0), + // 東京メトロ有楽町線 地下鉄成増〜地下鉄赤塚: 東京メトロ GTFS 90本 平均1.8分 (路線値 65km/h) + (28006, 2800602, 2800603, 76.0), + // 東京メトロ有楽町線 地下鉄赤塚〜平和台: 東京メトロ GTFS 90本 平均2.1分 (路線値 65km/h) + (28006, 2800603, 2800604, 71.0), + // 東京メトロ有楽町線 氷川台〜小竹向原: 東京メトロ GTFS 90本 平均1.6分 (路線値 65km/h) + (28006, 2800605, 2800606, 101.0), + // 東京メトロ有楽町線 小竹向原〜千川: 東京メトロ GTFS 142本 平均1.6分 (路線値 65km/h) + (28006, 2800606, 2800607, 74.0), + // 東京メトロ有楽町線 千川〜要町: 東京メトロ GTFS 142本 平均1.4分 (路線値 65km/h) + (28006, 2800607, 2800608, 103.0), + // 東京メトロ有楽町線 池袋〜東池袋: 東京メトロ GTFS 143本 平均1.7分 (路線値 65km/h) + (28006, 2800609, 2800610, 49.0), + // 東京メトロ有楽町線 東池袋〜護国寺: 東京メトロ GTFS 143本 平均1.4分 (路線値 65km/h) + (28006, 2800610, 2800611, 104.0), + // 東京メトロ有楽町線 江戸川橋〜飯田橋: 東京メトロ GTFS 143本 平均2.4分 (路線値 65km/h) + (28006, 2800612, 2800613, 41.0), + // 東京メトロ有楽町線 市ケ谷〜麹町: 東京メトロ GTFS 143本 平均1.3分 (路線値 65km/h) + (28006, 2800614, 2800615, 93.0), + // 東京メトロ有楽町線 麹町〜永田町: 東京メトロ GTFS 143本 平均1.2分 (路線値 65km/h) + (28006, 2800615, 2800616, 49.0), + // 東京メトロ有楽町線 永田町〜桜田門: 東京メトロ GTFS 143本 平均1.4分 (路線値 65km/h) + (28006, 2800616, 2800617, 94.0), + // 東京メトロ有楽町線 桜田門〜有楽町: 東京メトロ GTFS 143本 平均1.4分 (路線値 65km/h) + (28006, 2800617, 2800618, 106.0), + // 東京メトロ有楽町線 銀座一丁目〜新富町: 東京メトロ GTFS 143本 平均1.2分 (路線値 65km/h) + (28006, 2800619, 2800620, 88.0), + // 東京メトロ有楽町線 新富町〜月島: 東京メトロ GTFS 143本 平均1.5分 (路線値 65km/h) + (28006, 2800620, 2800621, 109.0), + // 東京メトロ有楽町線 月島〜豊洲: 東京メトロ GTFS 143本 平均1.9分 (路線値 65km/h) + (28006, 2800621, 2800622, 82.0), + // 東京メトロ有楽町線 豊洲〜辰巳: 東京メトロ GTFS 143本 平均1.9分 (路線値 65km/h) + (28006, 2800622, 2800623, 85.0), + // 東京メトロ半蔵門線 渋谷〜表参道: 東京メトロ GTFS 184本 平均1.7分 (路線値 55km/h) + (28008, 2800801, 2800802, 85.0), + // 東京メトロ半蔵門線 表参道〜青山一丁目: 東京メトロ GTFS 184本 平均1.7分 (路線値 55km/h) + (28008, 2800802, 2800803, 107.0), + // 東京メトロ半蔵門線 青山一丁目〜永田町: 東京メトロ GTFS 184本 平均1.9分 (路線値 55km/h) + (28008, 2800803, 2800804, 95.0), + // 東京メトロ半蔵門線 永田町〜半蔵門: 東京メトロ GTFS 184本 平均1.3分 (路線値 55km/h) + (28008, 2800804, 2800805, 74.0), + // 東京メトロ半蔵門線 九段下〜神保町: 東京メトロ GTFS 184本 平均1.0分 (路線値 55km/h) + (28008, 2800806, 2800807, 75.0), + // 東京メトロ半蔵門線 神保町〜大手町: 東京メトロ GTFS 184本 平均1.5分 (路線値 55km/h) + (28008, 2800807, 2800808, 105.0), + // 東京メトロ半蔵門線 大手町〜三越前: 東京メトロ GTFS 184本 平均1.3分 (路線値 55km/h) + (28008, 2800808, 2800809, 93.0), + // 東京メトロ半蔵門線 三越前〜水天宮前: 東京メトロ GTFS 184本 平均2.3分 (路線値 55km/h) + (28008, 2800809, 2800810, 42.0), + // 東京メトロ半蔵門線 水天宮前〜清澄白河: 東京メトロ GTFS 184本 平均2.4分 (路線値 55km/h) + (28008, 2800810, 2800811, 43.0), + // 東京メトロ半蔵門線 清澄白河〜住吉: 東京メトロ GTFS 183本 平均2.4分 (路線値 55km/h) + (28008, 2800811, 2800814, 65.0), + // 東京メトロ半蔵門線 錦糸町〜押上〈スカイツリー前〉: 東京メトロ GTFS 183本 平均1.9分 (路線値 55km/h) + (28008, 2800812, 2800813, 87.0), + // 東京メトロ半蔵門線 錦糸町〜住吉: 東京メトロ GTFS 183本 平均1.4分 (路線値 55km/h) + (28008, 2800812, 2800814, 102.0), + // 東京メトロ南北線 赤羽岩淵〜志茂: 東京メトロ GTFS 140本 平均1.5分 (路線値 65km/h) + (28009, 2800901, 2800902, 109.0), + // 東京メトロ南北線 志茂〜王子神谷: 東京メトロ GTFS 140本 平均1.9分 (路線値 65km/h) + (28009, 2800902, 2800903, 71.0), + // 東京メトロ南北線 王子〜西ケ原: 東京メトロ GTFS 140本 平均1.4分 (路線値 65km/h) + (28009, 2800904, 2800905, 102.0), + // 東京メトロ南北線 西ケ原〜駒込: 東京メトロ GTFS 140本 平均1.4分 (路線値 65km/h) + (28009, 2800905, 2800906, 106.0), + // 東京メトロ南北線 駒込〜本駒込: 東京メトロ GTFS 140本 平均2.1分 (路線値 65km/h) + (28009, 2800906, 2800907, 70.0), + // 東京メトロ南北線 本駒込〜東大前: 東京メトロ GTFS 140本 平均1.3分 (路線値 65km/h) + (28009, 2800907, 2800908, 93.0), + // 東京メトロ南北線 東大前〜後楽園: 東京メトロ GTFS 140本 平均1.5分 (路線値 65km/h) + (28009, 2800908, 2800909, 104.0), + // 東京メトロ南北線 後楽園〜飯田橋: 東京メトロ GTFS 140本 平均1.7分 (路線値 65km/h) + (28009, 2800909, 2800910, 55.0), + // 東京メトロ南北線 市ケ谷〜四ツ谷: 東京メトロ GTFS 140本 平均1.4分 (路線値 65km/h) + (28009, 2800911, 2800912, 102.0), + // 東京メトロ南北線 四ツ谷〜永田町: 東京メトロ GTFS 140本 平均1.6分 (路線値 65km/h) + (28009, 2800912, 2800913, 107.0), + // 東京メトロ南北線 永田町〜溜池山王: 東京メトロ GTFS 140本 平均1.1分 (路線値 65km/h) + (28009, 2800913, 2800914, 78.0), + // 東京メトロ南北線 溜池山王〜六本木一丁目: 東京メトロ GTFS 140本 平均1.7分 (路線値 65km/h) + (28009, 2800914, 2800915, 50.0), + // 東京メトロ南北線 麻布十番〜白金高輪: 東京メトロ GTFS 140本 平均1.7分 (路線値 65km/h) + (28009, 2800916, 2800917, 81.0), + // 東京メトロ南北線 白金高輪〜白金台: 東京メトロ GTFS 79本 平均1.7分 (路線値 65km/h) + (28009, 2800917, 2800918, 48.0), + // 東京メトロ南北線 白金台〜目黒: 東京メトロ GTFS 79本 平均1.9分 (路線値 65km/h) + (28009, 2800918, 2800919, 50.0), + // 東京メトロ副都心線 和光市〜地下鉄成増: 東京メトロ GTFS 82本 平均2.6分 (路線値 55km/h) + (28010, 2801001, 2801002, 75.0), + // 東京メトロ副都心線 地下鉄成増〜地下鉄赤塚: 東京メトロ GTFS 82本 平均1.8分 (路線値 55km/h) + (28010, 2801002, 2801003, 79.0), + // 東京メトロ副都心線 地下鉄赤塚〜平和台: 東京メトロ GTFS 82本 平均2.0分 (路線値 55km/h) + (28010, 2801003, 2801004, 77.0), + // 東京メトロ副都心線 小竹向原〜千川: 東京メトロ GTFS 115本 平均1.7分 (路線値 55km/h) + (28010, 2801006, 2801007, 65.0), + // 東京メトロ副都心線 千川〜要町: 東京メトロ GTFS 115本 平均1.4分 (路線値 55km/h) + (28010, 2801007, 2801008, 104.0), + // 東京メトロ副都心線 要町〜池袋: 東京メトロ GTFS 115本 平均1.3分 (路線値 55km/h) + (28010, 2801008, 2801009, 97.0), + // 東京メトロ副都心線 池袋〜雑司が谷: 東京メトロ GTFS 136本 平均1.9分 (路線値 55km/h) + (28010, 2801009, 2801010, 72.0), + // 東京メトロ副都心線 西早稲田〜東新宿: 東京メトロ GTFS 136本 平均1.4分 (路線値 55km/h) + (28010, 2801011, 2801012, 105.0), + // 東京メトロ副都心線 東新宿〜新宿三丁目: 東京メトロ GTFS 136本 平均1.4分 (路線値 55km/h) + (28010, 2801012, 2801013, 72.0), + // 東京メトロ副都心線 新宿三丁目〜北参道: 東京メトロ GTFS 138本 平均2.0分 (路線値 55km/h) + (28010, 2801013, 2801014, 62.0), + // 東京メトロ副都心線 北参道〜明治神宮前〈原宿〉: 東京メトロ GTFS 138本 平均1.5分 (路線値 55km/h) + (28010, 2801014, 2801015, 108.0), + // 東京メトロ副都心線 明治神宮前〈原宿〉〜渋谷: 東京メトロ GTFS 138本 平均1.6分 (路線値 55km/h) + (28010, 2801015, 2801016, 59.0), + // 函館市電2系統 湯の川〜湯の川温泉: 函館市電 GTFS 53本 平均1.6分 (路線値 20km/h) + (99105, 9910501, 9910502, 23.0), + // 函館市電2系統 函館アリーナ前〜駒場車庫前: 函館市電 GTFS 53本 平均0.9分 (路線値 20km/h) + (99105, 9910503, 9910504, 29.0), + // 函館市電2系統 競馬場前〜深堀町: 函館市電 GTFS 53本 平均1.7分 (路線値 20km/h) + (99105, 9910505, 9910506, 26.0), + // 函館市電2系統 深堀町〜柏木町: 函館市電 GTFS 53本 平均1.4分 (路線値 20km/h) + (99105, 9910506, 9910507, 25.0), + // 函館市電2系統 杉並町〜五稜郭公園前: 函館市電 GTFS 53本 平均1.9分 (路線値 20km/h) + (99105, 9910508, 9910509, 28.0), + // 函館市電2系統 五稜郭公園前〜中央病院前: 函館市電 GTFS 53本 平均2.3分 (路線値 20km/h) + (99105, 9910509, 9910510, 9.0), + // 函館市電2系統 中央病院前〜千代台: 函館市電 GTFS 53本 平均1.4分 (路線値 20km/h) + (99105, 9910510, 9910511, 23.0), + // 函館市電2系統 堀川町〜昭和橋: 函館市電 GTFS 53本 平均1.4分 (路線値 20km/h) + (99105, 9910512, 9910513, 23.0), + // 函館市電2系統 千歳町〜新川町: 函館市電 GTFS 53本 平均0.9分 (路線値 20km/h) + (99105, 9910514, 9910515, 24.0), + // 函館市電2系統 新川町〜松風町: 函館市電 GTFS 53本 平均1.4分 (路線値 20km/h) + (99105, 9910515, 9910516, 31.0), + // 函館市電2系統 松風町〜函館駅前: 函館市電 GTFS 53本 平均2.7分 (路線値 20km/h) + (99105, 9910516, 9910517, 13.0), + // 函館市電2系統 函館駅前〜市役所前: 函館市電 GTFS 53本 平均1.4分 (路線値 20km/h) + (99105, 9910517, 9910518, 32.0), + // 函館市電2系統 十字街〜宝来町: 函館市電 GTFS 53本 平均2.0分 (路線値 20km/h) + (99105, 9910520, 9910521, 14.0), + // 函館市電2系統 宝来町〜青柳町: 函館市電 GTFS 53本 平均1.7分 (路線値 20km/h) + (99105, 9910521, 9910522, 28.0), + // 函館市電2系統 青柳町〜谷地頭: 函館市電 GTFS 53本 平均2.2分 (路線値 20km/h) + (99105, 9910522, 9910523, 13.0), + // 函館市電5系統 湯の川〜湯の川温泉: 函館市電 GTFS 53本 平均1.6分 (路線値 25km/h) + (99106, 9910601, 9910602, 23.0), + // 函館市電5系統 函館アリーナ前〜駒場車庫前: 函館市電 GTFS 53本 平均0.9分 (路線値 25km/h) + (99106, 9910603, 9910604, 29.0), + // 函館市電5系統 柏木町〜杉並町: 函館市電 GTFS 53本 平均1.4分 (路線値 25km/h) + (99106, 9910607, 9910608, 32.0), + // 函館市電5系統 杉並町〜五稜郭公園前: 函館市電 GTFS 53本 平均1.7分 (路線値 25km/h) + (99106, 9910608, 9910609, 33.0), + // 函館市電5系統 中央病院前〜千代台: 函館市電 GTFS 53本 平均1.4分 (路線値 25km/h) + (99106, 9910610, 9910611, 23.0), + // 函館市電5系統 千代台〜堀川町: 函館市電 GTFS 53本 平均1.4分 (路線値 25km/h) + (99106, 9910611, 9910612, 40.0), + // 函館市電5系統 堀川町〜昭和橋: 函館市電 GTFS 53本 平均1.4分 (路線値 25km/h) + (99106, 9910612, 9910613, 23.0), + // 函館市電5系統 昭和橋〜千歳町: 函館市電 GTFS 53本 平均0.9分 (路線値 25km/h) + (99106, 9910613, 9910614, 31.0), + // 函館市電5系統 新川町〜松風町: 函館市電 GTFS 53本 平均1.3分 (路線値 25km/h) + (99106, 9910615, 9910616, 32.0), + // 函館市電5系統 松風町〜函館駅前: 函館市電 GTFS 53本 平均2.7分 (路線値 25km/h) + (99106, 9910616, 9910617, 13.0), + // 函館市電5系統 函館駅前〜市役所前: 函館市電 GTFS 53本 平均1.4分 (路線値 25km/h) + (99106, 9910617, 9910618, 31.0), + // 函館市電5系統 魚市場通〜十字街: 函館市電 GTFS 53本 平均1.4分 (路線値 25km/h) + (99106, 9910619, 9910620, 20.0), + // 函館市電5系統 十字街〜末広町: 函館市電 GTFS 53本 平均3.0分 (路線値 25km/h) + (99106, 9910620, 9910621, 17.0), + // 函館市電5系統 大町〜函館どつく前: 函館市電 GTFS 53本 平均1.4分 (路線値 25km/h) + (99106, 9910622, 9910623, 32.0), + // 都営大江戸線 都庁前〜新宿西口: 都営地下鉄 GTFS 145本 平均1.0分 (路線値 70km/h) + (99301, 9930100, 9930102, 87.0), + // 都営大江戸線 都庁前〜新宿: 都営地下鉄 GTFS 146本 平均1.6分 (路線値 70km/h) + (99301, 9930101, 9930128, 32.0), + // 都営大江戸線 都庁前〜西新宿五丁目: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 70km/h) + (99301, 9930101, 9930129, 92.0), + // 都営大江戸線 新宿西口〜東新宿: 都営地下鉄 GTFS 145本 平均2.0分 (路線値 70km/h) + (99301, 9930102, 9930103, 40.0), + // 都営大江戸線 東新宿〜若松河田: 都営地下鉄 GTFS 145本 平均1.5分 (路線値 70km/h) + (99301, 9930103, 9930104, 74.0), + // 都営大江戸線 若松河田〜牛込柳町: 都営地下鉄 GTFS 145本 平均1.0分 (路線値 70km/h) + (99301, 9930104, 9930105, 83.0), + // 都営大江戸線 牛込柳町〜牛込神楽坂: 都営地下鉄 GTFS 145本 平均1.2分 (路線値 70km/h) + (99301, 9930105, 9930106, 105.0), + // 都営大江戸線 牛込神楽坂〜飯田橋: 都営地下鉄 GTFS 145本 平均2.0分 (路線値 70km/h) + (99301, 9930106, 9930107, 37.0), + // 都営大江戸線 飯田橋〜春日: 都営地下鉄 GTFS 145本 平均1.4分 (路線値 70km/h) + (99301, 9930107, 9930108, 109.0), + // 都営大江戸線 春日〜本郷三丁目: 都営地下鉄 GTFS 145本 平均1.0分 (路線値 70km/h) + (99301, 9930108, 9930109, 86.0), + // 都営大江戸線 本郷三丁目〜上野御徒町: 都営地下鉄 GTFS 145本 平均2.0分 (路線値 70km/h) + (99301, 9930109, 9930110, 62.0), + // 都営大江戸線 上野御徒町〜新御徒町: 都営地下鉄 GTFS 145本 平均1.0分 (路線値 70km/h) + (99301, 9930110, 9930111, 88.0), + // 都営大江戸線 新御徒町〜蔵前: 都営地下鉄 GTFS 145本 平均1.0分 (路線値 70km/h) + (99301, 9930111, 9930112, 101.0), + // 都営大江戸線 蔵前〜両国: 都営地下鉄 GTFS 145本 平均2.0分 (路線値 70km/h) + (99301, 9930112, 9930113, 40.0), + // 都営大江戸線 両国〜森下: 都営地下鉄 GTFS 145本 平均1.0分 (路線値 70km/h) + (99301, 9930113, 9930114, 106.0), + // 都営大江戸線 森下〜清澄白河: 都営地下鉄 GTFS 145本 平均1.0分 (路線値 70km/h) + (99301, 9930114, 9930115, 87.0), + // 都営大江戸線 清澄白河〜門前仲町: 都営地下鉄 GTFS 146本 平均2.0分 (路線値 70km/h) + (99301, 9930115, 9930116, 54.0), + // 都営大江戸線 月島〜勝どき: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 70km/h) + (99301, 9930117, 9930118, 106.0), + // 都営大江戸線 勝どき〜築地市場: 都営地下鉄 GTFS 146本 平均2.0分 (路線値 70km/h) + (99301, 9930118, 9930119, 51.0), + // 都営大江戸線 築地市場〜汐留: 都営地下鉄 GTFS 146本 平均1.7分 (路線値 70km/h) + (99301, 9930119, 9930120, 30.0), + // 都営大江戸線 汐留〜大門: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 70km/h) + (99301, 9930120, 9930121, 103.0), + // 都営大江戸線 大門〜赤羽橋: 都営地下鉄 GTFS 146本 平均2.0分 (路線値 70km/h) + (99301, 9930121, 9930122, 45.0), + // 都営大江戸線 赤羽橋〜麻布十番: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 70km/h) + (99301, 9930122, 9930123, 89.0), + // 都営大江戸線 六本木〜青山一丁目: 都営地下鉄 GTFS 146本 平均2.0分 (路線値 70km/h) + (99301, 9930124, 9930125, 57.0), + // 都営大江戸線 青山一丁目〜国立競技場: 都営地下鉄 GTFS 146本 平均2.0分 (路線値 70km/h) + (99301, 9930125, 9930126, 51.0), + // 都営大江戸線 国立競技場〜代々木: 都営地下鉄 GTFS 146本 平均2.0分 (路線値 70km/h) + (99301, 9930126, 9930127, 60.0), + // 都営大江戸線 代々木〜新宿: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 70km/h) + (99301, 9930127, 9930128, 86.0), + // 都営大江戸線 西新宿五丁目〜中野坂上: 都営地下鉄 GTFS 146本 平均2.0分 (路線値 70km/h) + (99301, 9930129, 9930130, 36.0), + // 都営大江戸線 中野坂上〜東中野: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 70km/h) + (99301, 9930130, 9930131, 111.0), + // 都営大江戸線 東中野〜中井: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 70km/h) + (99301, 9930131, 9930132, 98.0), + // 都営大江戸線 中井〜落合南長崎: 都営地下鉄 GTFS 146本 平均2.0分 (路線値 70km/h) + (99301, 9930132, 9930133, 50.0), + // 都営大江戸線 落合南長崎〜新江古田: 都営地下鉄 GTFS 146本 平均2.0分 (路線値 70km/h) + (99301, 9930133, 9930134, 78.0), + // 都営大江戸線 新江古田〜練馬: 都営地下鉄 GTFS 146本 平均2.0分 (路線値 70km/h) + (99301, 9930134, 9930135, 80.0), + // 都営大江戸線 練馬〜豊島園: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 70km/h) + (99301, 9930135, 9930136, 93.0), + // 都営大江戸線 豊島園〜練馬春日町: 都営地下鉄 GTFS 146本 平均2.0分 (路線値 70km/h) + (99301, 9930136, 9930137, 60.0), + // 都営浅草線 西馬込〜馬込: 都営地下鉄 GTFS 100本 平均1.6分 (路線値 65km/h) + (99302, 9930201, 9930202, 75.0), + // 都営浅草線 馬込〜中延: 都営地下鉄 GTFS 100本 平均1.0分 (路線値 65km/h) + (99302, 9930202, 9930203, 104.0), + // 都営浅草線 中延〜戸越: 都営地下鉄 GTFS 100本 平均1.0分 (路線値 65km/h) + (99302, 9930203, 9930204, 103.0), + // 都営浅草線 戸越〜五反田: 都営地下鉄 GTFS 100本 平均2.0分 (路線値 65km/h) + (99302, 9930204, 9930205, 72.0), + // 都営浅草線 五反田〜高輪台: 都営地下鉄 GTFS 100本 平均1.0分 (路線値 65km/h) + (99302, 9930205, 9930206, 88.0), + // 都営浅草線 高輪台〜泉岳寺: 都営地下鉄 GTFS 100本 平均2.2分 (路線値 65km/h) + (99302, 9930206, 9930207, 43.0), + // 都営浅草線 三田〜大門: 都営地下鉄 GTFS 166本 平均1.8分 (路線値 65km/h) + (99302, 9930208, 9930209, 53.0), + // 都営浅草線 大門〜新橋: 都営地下鉄 GTFS 166本 平均1.3分 (路線値 65km/h) + (99302, 9930209, 9930210, 105.0), + // 都営浅草線 新橋〜東銀座: 都営地下鉄 GTFS 166本 平均1.7分 (路線値 65km/h) + (99302, 9930210, 9930211, 40.0), + // 都営浅草線 東銀座〜宝町: 都営地下鉄 GTFS 166本 平均1.0分 (路線値 65km/h) + (99302, 9930211, 9930212, 90.0), + // 都営浅草線 宝町〜日本橋: 都営地下鉄 GTFS 166本 平均1.0分 (路線値 65km/h) + (99302, 9930212, 9930213, 90.0), + // 都営浅草線 日本橋〜人形町: 都営地下鉄 GTFS 166本 平均1.0分 (路線値 65km/h) + (99302, 9930213, 9930214, 90.0), + // 都営浅草線 人形町〜東日本橋: 都営地下鉄 GTFS 166本 平均1.0分 (路線値 65km/h) + (99302, 9930214, 9930215, 84.0), + // 都営浅草線 東日本橋〜浅草橋: 都営地下鉄 GTFS 166本 平均1.0分 (路線値 65km/h) + (99302, 9930215, 9930216, 79.0), + // 都営浅草線 浅草橋〜蔵前: 都営地下鉄 GTFS 166本 平均1.0分 (路線値 65km/h) + (99302, 9930216, 9930217, 89.0), + // 都営浅草線 蔵前〜浅草: 都営地下鉄 GTFS 166本 平均1.0分 (路線値 65km/h) + (99302, 9930217, 9930218, 96.0), + // 都営浅草線 浅草〜本所吾妻橋: 都営地下鉄 GTFS 166本 平均1.0分 (路線値 65km/h) + (99302, 9930218, 9930219, 85.0), + // 都営浅草線 本所吾妻橋〜押上(スカイツリー前): 都営地下鉄 GTFS 166本 平均1.0分 (路線値 65km/h) + (99302, 9930219, 9930220, 90.0), + // 都営三田線 目黒〜白金台: 都営地下鉄 GTFS 90本 平均2.0分 (路線値 75km/h) + (99303, 9930301, 9930302, 46.0), + // 都営三田線 白金台〜白金高輪: 都営地下鉄 GTFS 90本 平均1.8分 (路線値 75km/h) + (99303, 9930302, 9930303, 43.0), + // 都営三田線 白金高輪〜三田: 都営地下鉄 GTFS 146本 平均2.8分 (路線値 75km/h) + (99303, 9930303, 9930304, 40.0), + // 都営三田線 三田〜芝公園: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) + (99303, 9930304, 9930305, 83.0), + // 都営三田線 芝公園〜御成門: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) + (99303, 9930305, 9930306, 92.0), + // 都営三田線 御成門〜内幸町: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) + (99303, 9930306, 9930307, 103.0), + // 都営三田線 内幸町〜日比谷: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) + (99303, 9930307, 9930308, 92.0), + // 都営三田線 日比谷〜大手町: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) + (99303, 9930308, 9930309, 103.0), + // 都営三田線 大手町〜神保町: 都営地下鉄 GTFS 146本 平均2.2分 (路線値 75km/h) + (99303, 9930309, 9930310, 48.0), + // 都営三田線 神保町〜水道橋: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) + (99303, 9930310, 9930311, 98.0), + // 都営三田線 水道橋〜春日: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) + (99303, 9930311, 9930312, 86.0), + // 都営三田線 春日〜白山: 都営地下鉄 GTFS 146本 平均1.6分 (路線値 75km/h) + (99303, 9930312, 9930313, 117.0), + // 都営三田線 白山〜千石: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) + (99303, 9930313, 9930314, 102.0), + // 都営三田線 千石〜巣鴨: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) + (99303, 9930314, 9930315, 94.0), + // 都営三田線 西巣鴨〜新板橋: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) + (99303, 9930316, 9930317, 101.0), + // 都営三田線 新板橋〜板橋区役所前: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) + (99303, 9930317, 9930318, 99.0), + // 都営三田線 板橋区役所前〜板橋本町: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) + (99303, 9930318, 9930319, 112.0), + // 都営三田線 板橋本町〜本蓮沼: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) + (99303, 9930319, 9930320, 96.0), + // 都営三田線 本蓮沼〜志村坂上: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) + (99303, 9930320, 9930321, 102.0), + // 都営三田線 志村坂上〜志村三丁目: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) + (99303, 9930321, 9930322, 96.0), + // 都営三田線 志村三丁目〜蓮根: 都営地下鉄 GTFS 146本 平均1.3分 (路線値 75km/h) + (99303, 9930322, 9930323, 102.0), + // 都営三田線 西台〜高島平: 都営地下鉄 GTFS 146本 平均1.1分 (路線値 75km/h) + (99303, 9930324, 9930325, 111.0), + // 都営三田線 高島平〜新高島平: 都営地下鉄 GTFS 140本 平均1.0分 (路線値 75km/h) + (99303, 9930325, 9930326, 82.0), + // 都営三田線 新高島平〜西高島平: 都営地下鉄 GTFS 140本 平均1.0分 (路線値 75km/h) + (99303, 9930326, 9930327, 90.0), + // 都営新宿線 新宿〜新宿三丁目: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) + (99304, 9930401, 9930402, 85.0), + // 都営新宿線 新宿三丁目〜曙橋: 都営地下鉄 GTFS 146本 平均2.0分 (路線値 75km/h) + (99304, 9930402, 9930403, 68.0), + // 都営新宿線 曙橋〜市ヶ谷: 都営地下鉄 GTFS 146本 平均2.0分 (路線値 75km/h) + (99304, 9930403, 9930404, 57.0), + // 都営新宿線 市ヶ谷〜九段下: 都営地下鉄 GTFS 146本 平均1.1分 (路線値 75km/h) + (99304, 9930404, 9930405, 118.0), + // 都営新宿線 九段下〜神保町: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) + (99304, 9930405, 9930406, 59.0), + // 都営新宿線 神保町〜小川町: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) + (99304, 9930406, 9930407, 92.0), + // 都営新宿線 小川町〜岩本町: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) + (99304, 9930407, 9930408, 92.0), + // 都営新宿線 岩本町〜馬喰横山: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) + (99304, 9930408, 9930409, 86.0), + // 都営新宿線 馬喰横山〜浜町: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) + (99304, 9930409, 9930410, 80.0), + // 都営新宿線 浜町〜森下: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) + (99304, 9930410, 9930411, 91.0), + // 都営新宿線 森下〜菊川: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) + (99304, 9930411, 9930412, 91.0), + // 都営新宿線 菊川〜住吉: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) + (99304, 9930412, 9930413, 95.0), + // 都営新宿線 住吉〜西大島: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) + (99304, 9930413, 9930414, 99.0), + // 都営新宿線 西大島〜大島: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) + (99304, 9930414, 9930415, 94.0), + // 都営新宿線 大島〜東大島: 都営地下鉄 GTFS 142本 平均2.0分 (路線値 75km/h) + (99304, 9930415, 9930416, 37.0), + // 都営新宿線 東大島〜船堀: 都営地下鉄 GTFS 142本 平均2.0分 (路線値 75km/h) + (99304, 9930416, 9930417, 90.0), + // 都営新宿線 船堀〜一之江: 都営地下鉄 GTFS 142本 平均2.0分 (路線値 75km/h) + (99304, 9930417, 9930418, 82.0), + // 都営新宿線 一之江〜瑞江: 都営地下鉄 GTFS 142本 平均2.0分 (路線値 75km/h) + (99304, 9930418, 9930419, 71.0), + // 都営新宿線 瑞江〜篠崎: 都営地下鉄 GTFS 142本 平均2.0分 (路線値 75km/h) + (99304, 9930419, 9930420, 68.0), + // 東京さくらトラム(都電荒川線) 三ノ輪橋〜荒川一中前: 都営地下鉄 GTFS 138本 平均1.2分 (路線値 25km/h) + (99305, 9930501, 9930502, 18.0), + // 東京さくらトラム(都電荒川線) 町屋駅前〜町屋二丁目: 都営地下鉄 GTFS 99本 平均2.4分 (路線値 25km/h) + (99305, 9930506, 9930507, 14.0), + // 東京さくらトラム(都電荒川線) 宮ノ前〜小台: 都営地下鉄 GTFS 102本 平均1.0分 (路線値 25km/h) + (99305, 9930510, 9930511, 27.0), + // 東京さくらトラム(都電荒川線) 小台〜荒川遊園地前: 都営地下鉄 GTFS 98本 平均1.0分 (路線値 25km/h) + (99305, 9930511, 9930512, 36.0), + // 東京さくらトラム(都電荒川線) 荒川遊園地前〜荒川車庫前: 都営地下鉄 GTFS 105本 平均3.5分 (路線値 25km/h) + (99305, 9930512, 9930513, 10.0), + // 東京さくらトラム(都電荒川線) 荒川車庫前〜梶原: 都営地下鉄 GTFS 99本 平均2.9分 (路線値 25km/h) + (99305, 9930513, 9930514, 13.0), + // 東京さくらトラム(都電荒川線) 栄町〜王子駅前: 都営地下鉄 GTFS 102本 平均3.0分 (路線値 25km/h) + (99305, 9930515, 9930516, 10.0), + // 東京さくらトラム(都電荒川線) 滝野川一丁目〜西ヶ原四丁目: 都営地下鉄 GTFS 103本 平均2.0分 (路線値 25km/h) + (99305, 9930518, 9930519, 16.0), + // 東京さくらトラム(都電荒川線) 西ヶ原四丁目〜新庚申塚: 都営地下鉄 GTFS 97本 平均2.0分 (路線値 25km/h) + (99305, 9930519, 9930520, 16.0), + // 東京さくらトラム(都電荒川線) 巣鴨新田〜大塚駅前: 都営地下鉄 GTFS 101本 平均3.0分 (路線値 25km/h) + (99305, 9930522, 9930523, 12.0), + // 東京さくらトラム(都電荒川線) 大塚駅前〜向原: 都営地下鉄 GTFS 97本 平均3.1分 (路線値 25km/h) + (99305, 9930523, 9930524, 12.0), + // 東京さくらトラム(都電荒川線) 向原〜東池袋四丁目: 都営地下鉄 GTFS 139本 平均1.6分 (路線値 25km/h) + (99305, 9930524, 9930525, 33.0), + // 東京さくらトラム(都電荒川線) 東池袋四丁目〜都電雑司ヶ谷: 都営地下鉄 GTFS 97本 平均1.0分 (路線値 25km/h) + (99305, 9930525, 9930526, 18.0), + // 東京さくらトラム(都電荒川線) 学習院下〜面影橋: 都営地下鉄 GTFS 139本 平均1.2分 (路線値 25km/h) + (99305, 9930528, 9930529, 32.0), + // 東京さくらトラム(都電荒川線) 面影橋〜早稲田: 都営地下鉄 GTFS 139本 平均1.2分 (路線値 25km/h) + (99305, 9930529, 9930530, 31.0), + // つくばエクスプレス線 秋葉原〜新御徒町: つくばエクスプレス GTFS 86本 平均2.1分 (路線値 95km/h) + (99309, 9930901, 9930902, 47.0), + // つくばエクスプレス線 新御徒町〜浅草: つくばエクスプレス GTFS 86本 平均1.9分 (路線値 95km/h) + (99309, 9930902, 9930903, 55.0), + // つくばエクスプレス線 浅草〜南千住: つくばエクスプレス GTFS 86本 平均2.4分 (路線値 95km/h) + (99309, 9930903, 9930904, 84.0), + // つくばエクスプレス線 南千住〜北千住: つくばエクスプレス GTFS 86本 平均2.0分 (路線値 95km/h) + (99309, 9930904, 9930905, 145.0), + // つくばエクスプレス線 北千住〜青井: つくばエクスプレス GTFS 86本 平均2.4分 (路線値 95km/h) + (99309, 9930905, 9930906, 141.0), + // つくばエクスプレス線 青井〜六町: つくばエクスプレス GTFS 86本 平均1.7分 (路線値 95km/h) + (99309, 9930906, 9930907, 125.0), + // つくばエクスプレス線 六町〜八潮: つくばエクスプレス GTFS 86本 平均3.0分 (路線値 95km/h) + (99309, 9930907, 9930908, 101.0), + // つくばエクスプレス線 八潮〜三郷中央: つくばエクスプレス GTFS 86本 平均2.8分 (路線値 95km/h) + (99309, 9930908, 9930909, 127.0), + // つくばエクスプレス線 三郷中央〜南流山: つくばエクスプレス GTFS 86本 平均2.4分 (路線値 95km/h) + (99309, 9930909, 9930910, 129.0), + // つくばエクスプレス線 南流山〜流山セントラルパーク: つくばエクスプレス GTFS 86本 平均2.0分 (路線値 95km/h) + (99309, 9930910, 9930911, 148.0), + // つくばエクスプレス線 流山セントラルパーク〜流山おおたかの森: つくばエクスプレス GTFS 86本 平均2.0分 (路線値 95km/h) + (99309, 9930911, 9930912, 150.0), + // つくばエクスプレス線 柏の葉キャンパス〜柏たなか: つくばエクスプレス GTFS 86本 平均2.0分 (路線値 95km/h) + (99309, 9930913, 9930914, 147.0), + // つくばエクスプレス線 柏たなか〜守谷: つくばエクスプレス GTFS 86本 平均4.0分 (路線値 95km/h) + (99309, 9930914, 9930915, 122.0), + // つくばエクスプレス線 守谷〜みらい平: つくばエクスプレス GTFS 28本 平均4.9分 (路線値 95km/h) + (99309, 9930915, 9930916, 111.0), + // つくばエクスプレス線 万博記念公園〜研究学園: つくばエクスプレス GTFS 28本 平均2.8分 (路線値 95km/h) + (99309, 9930918, 9930919, 127.0), + // 横浜市営地下鉄ブルーライン 中川〜センター北: 横浜市営地下鉄 GTFS 69本 平均1.9分 (路線値 75km/h) + (99316, 9931602, 9931603, 62.0), + // 横浜市営地下鉄ブルーライン センター北〜センター南: 横浜市営地下鉄 GTFS 69本 平均1.0分 (路線値 75km/h) + (99316, 9931603, 9931604, 102.0), + // 横浜市営地下鉄ブルーライン センター南〜仲町台: 横浜市営地下鉄 GTFS 69本 平均2.1分 (路線値 75km/h) + (99316, 9931604, 9931605, 81.0), + // 横浜市営地下鉄ブルーライン 仲町台〜新羽: 横浜市営地下鉄 GTFS 69本 平均2.4分 (路線値 75km/h) + (99316, 9931605, 9931606, 84.0), + // 横浜市営地下鉄ブルーライン 新羽〜北新横浜: 横浜市営地下鉄 GTFS 94本 平均1.5分 (路線値 75km/h) + (99316, 9931606, 9931607, 64.0), + // 横浜市営地下鉄ブルーライン 北新横浜〜新横浜: 横浜市営地下鉄 GTFS 94本 平均1.2分 (路線値 75km/h) + (99316, 9931607, 9931608, 116.0), + // 横浜市営地下鉄ブルーライン 新横浜〜岸根公園: 横浜市営地下鉄 GTFS 94本 平均2.0分 (路線値 75km/h) + (99316, 9931608, 9931609, 64.0), + // 横浜市営地下鉄ブルーライン 岸根公園〜片倉町: 横浜市営地下鉄 GTFS 94本 平均1.0分 (路線値 75km/h) + (99316, 9931609, 9931610, 111.0), + // 横浜市営地下鉄ブルーライン 三ツ沢上町〜三ツ沢下町: 横浜市営地下鉄 GTFS 94本 平均1.0分 (路線値 75km/h) + (99316, 9931611, 9931612, 95.0), + // 横浜市営地下鉄ブルーライン 三ツ沢下町〜横浜: 横浜市営地下鉄 GTFS 94本 平均2.0分 (路線値 75km/h) + (99316, 9931612, 9931613, 57.0), + // 横浜市営地下鉄ブルーライン 横浜〜高島町: 横浜市営地下鉄 GTFS 94本 平均1.0分 (路線値 75km/h) + (99316, 9931613, 9931614, 95.0), + // 横浜市営地下鉄ブルーライン 高島町〜桜木町: 横浜市営地下鉄 GTFS 94本 平均1.0分 (路線値 75km/h) + (99316, 9931614, 9931615, 113.0), + // 横浜市営地下鉄ブルーライン 桜木町〜関内: 横浜市営地下鉄 GTFS 94本 平均1.0分 (路線値 75km/h) + (99316, 9931615, 9931616, 82.0), + // 横浜市営地下鉄ブルーライン 伊勢佐木長者町〜阪東橋: 横浜市営地下鉄 GTFS 94本 平均1.0分 (路線値 75km/h) + (99316, 9931617, 9931618, 95.0), + // 横浜市営地下鉄ブルーライン 阪東橋〜吉野町: 横浜市営地下鉄 GTFS 94本 平均1.0分 (路線値 75km/h) + (99316, 9931618, 9931619, 81.0), + // 横浜市営地下鉄ブルーライン 吉野町〜蒔田: 横浜市営地下鉄 GTFS 94本 平均1.0分 (路線値 75km/h) + (99316, 9931619, 9931620, 102.0), + // 横浜市営地下鉄ブルーライン 蒔田〜弘明寺: 横浜市営地下鉄 GTFS 94本 平均1.0分 (路線値 75km/h) + (99316, 9931620, 9931621, 105.0), + // 横浜市営地下鉄ブルーライン 弘明寺〜上大岡: 横浜市営地下鉄 GTFS 94本 平均2.0分 (路線値 75km/h) + (99316, 9931621, 9931622, 87.0), + // 横浜市営地下鉄ブルーライン 上大岡〜港南中央: 横浜市営地下鉄 GTFS 94本 平均1.0分 (路線値 75km/h) + (99316, 9931622, 9931623, 101.0), + // 横浜市営地下鉄ブルーライン 港南中央〜上永谷: 横浜市営地下鉄 GTFS 94本 平均2.4分 (路線値 75km/h) + (99316, 9931623, 9931624, 60.0), + // 横浜市営地下鉄ブルーライン 上永谷〜下永谷: 横浜市営地下鉄 GTFS 94本 平均1.9分 (路線値 75km/h) + (99316, 9931624, 9931625, 59.0), + // 横浜市営地下鉄ブルーライン 下永谷〜舞岡: 横浜市営地下鉄 GTFS 94本 平均1.0分 (路線値 75km/h) + (99316, 9931625, 9931626, 87.0), + // 横浜市営地下鉄ブルーライン 戸塚〜踊場: 横浜市営地下鉄 GTFS 94本 平均1.8分 (路線値 75km/h) + (99316, 9931627, 9931628, 105.0), + // 横浜市営地下鉄ブルーライン 踊場〜中田: 横浜市営地下鉄 GTFS 71本 平均1.0分 (路線値 75km/h) + (99316, 9931628, 9931629, 95.0), + // 横浜市営地下鉄ブルーライン 中田〜立場: 横浜市営地下鉄 GTFS 71本 平均1.0分 (路線値 75km/h) + (99316, 9931629, 9931630, 106.0), + // 横浜市営地下鉄ブルーライン 立場〜下飯田: 横浜市営地下鉄 GTFS 71本 平均2.0分 (路線値 75km/h) + (99316, 9931630, 9931631, 114.0), + // 横浜市営地下鉄ブルーライン 下飯田〜湘南台: 横浜市営地下鉄 GTFS 71本 平均2.0分 (路線値 75km/h) + (99316, 9931631, 9931632, 81.0), + // 多摩モノレール 多摩センター〜松が谷: 多摩都市モノレール GTFS 88本 平均1.8分 (路線値 50km/h) + (99334, 9933401, 9933402, 41.0), + // 多摩モノレール 大塚・帝京大学〜中央大学・明星大学: 多摩都市モノレール GTFS 88本 平均1.4分 (路線値 50km/h) + (99334, 9933403, 9933404, 72.0), + // 多摩モノレール 中央大学・明星大学〜多摩動物公園: 多摩都市モノレール GTFS 88本 平均2.0分 (路線値 50km/h) + (99334, 9933404, 9933405, 34.0), + // 多摩モノレール 多摩動物公園〜程久保: 多摩都市モノレール GTFS 88本 平均1.4分 (路線値 50km/h) + (99334, 9933405, 9933406, 75.0), + // 多摩モノレール 程久保〜高幡不動: 多摩都市モノレール GTFS 88本 平均1.4分 (路線値 50km/h) + (99334, 9933406, 9933407, 56.0), + // 多摩モノレール 高幡不動〜万願寺: 多摩都市モノレール GTFS 88本 平均2.2分 (路線値 50km/h) + (99334, 9933407, 9933408, 42.0), + // 多摩モノレール 柴崎体育館〜立川南: 多摩都市モノレール GTFS 88本 平均1.3分 (路線値 50km/h) + (99334, 9933410, 9933411, 62.0), + // 多摩モノレール 立川南〜立川北: 多摩都市モノレール GTFS 88本 平均0.9分 (路線値 50km/h) + (99334, 9933411, 9933412, 63.0), + // 多摩モノレール 立川北〜高松: 多摩都市モノレール GTFS 88本 平均1.7分 (路線値 50km/h) + (99334, 9933412, 9933413, 70.0), + // 多摩モノレール 高松〜立飛: 多摩都市モノレール GTFS 87本 平均1.1分 (路線値 50km/h) + (99334, 9933413, 9933414, 61.0), + // 多摩モノレール 立飛〜泉体育館: 多摩都市モノレール GTFS 87本 平均1.1分 (路線値 50km/h) + (99334, 9933414, 9933415, 45.0), + // 多摩モノレール 泉体育館〜砂川七番: 多摩都市モノレール GTFS 87本 平均1.0分 (路線値 50km/h) + (99334, 9933415, 9933416, 74.0), + // 多摩モノレール 桜街道〜上北台: 多摩都市モノレール GTFS 87本 平均1.5分 (路線値 50km/h) + (99334, 9933418, 9933419, 45.0), + // りんかい線 新木場〜東雲: りんかい線 GTFS 106本 平均2.3分 (路線値 65km/h) + (99337, 9933701, 9933702, 96.0), + // りんかい線 国際展示場〜東京テレポート: りんかい線 GTFS 106本 平均1.9分 (路線値 65km/h) + (99337, 9933703, 9933704, 72.0), + // りんかい線 東京テレポート〜天王洲アイル: りんかい線 GTFS 104本 平均2.9分 (路線値 65km/h) + (99337, 9933704, 9933705, 79.0), + // りんかい線 品川シーサイド〜大井町: りんかい線 GTFS 104本 平均2.0分 (路線値 65km/h) + (99337, 9933706, 9933707, 61.0), + // りんかい線 大井町〜大崎: りんかい線 GTFS 104本 平均2.7分 (路線値 65km/h) + (99337, 9933707, 9933708, 44.0), + // 日暮里・舎人ライナー 日暮里〜西日暮里: 都営地下鉄 GTFS 151本 平均1.0分 (路線値 60km/h) + (99342, 9934201, 9934202, 78.0), + // 日暮里・舎人ライナー 赤土小学校前〜熊野前: 都営地下鉄 GTFS 151本 平均1.0分 (路線値 60km/h) + (99342, 9934203, 9934204, 86.0), + // 日暮里・舎人ライナー 熊野前〜足立小台: 都営地下鉄 GTFS 151本 平均1.0分 (路線値 60km/h) + (99342, 9934204, 9934205, 81.0), + // 日暮里・舎人ライナー 扇大橋〜高野: 都営地下鉄 GTFS 141本 平均1.0分 (路線値 60km/h) + (99342, 9934206, 9934207, 54.0), + // 日暮里・舎人ライナー 高野〜江北: 都営地下鉄 GTFS 151本 平均1.0分 (路線値 60km/h) + (99342, 9934207, 9934208, 80.0), + // 日暮里・舎人ライナー 江北〜西新井大師西: 都営地下鉄 GTFS 151本 平均1.0分 (路線値 60km/h) + (99342, 9934208, 9934209, 92.0), + // 日暮里・舎人ライナー 西新井大師西〜谷在家: 都営地下鉄 GTFS 151本 平均1.0分 (路線値 60km/h) + (99342, 9934209, 9934210, 91.0), + // 日暮里・舎人ライナー 谷在家〜舎人公園: 都営地下鉄 GTFS 151本 平均1.1分 (路線値 60km/h) + (99342, 9934210, 9934211, 92.0), + // 横浜市営地下鉄グリーンライン 川和町〜都筑ふれあいの丘: 横浜市営地下鉄 GTFS 85本 平均2.0分 (路線値 75km/h) + (99343, 9934302, 9934303, 67.0), + // 横浜市営地下鉄グリーンライン 都筑ふれあいの丘〜センター南: 横浜市営地下鉄 GTFS 85本 平均2.0分 (路線値 75km/h) + (99343, 9934303, 9934304, 71.0), + // 横浜市営地下鉄グリーンライン センター南〜センター北: 横浜市営地下鉄 GTFS 85本 平均1.0分 (路線値 75km/h) + (99343, 9934304, 9934305, 98.0), + // 横浜市営地下鉄グリーンライン センター北〜北山田: 横浜市営地下鉄 GTFS 85本 平均2.0分 (路線値 75km/h) + (99343, 9934305, 9934306, 68.0), + // 横浜市営地下鉄グリーンライン 北山田〜東山田: 横浜市営地下鉄 GTFS 85本 平均1.6分 (路線値 75km/h) + (99343, 9934306, 9934307, 99.0), + // 横浜市営地下鉄グリーンライン 東山田〜高田: 横浜市営地下鉄 GTFS 85本 平均2.0分 (路線値 75km/h) + (99343, 9934307, 9934308, 69.0), + // 横浜市営地下鉄グリーンライン 高田〜日吉本町: 横浜市営地下鉄 GTFS 85本 平均1.0分 (路線値 75km/h) + (99343, 9934308, 9934309, 112.0), + // 横浜市営地下鉄グリーンライン 日吉本町〜日吉: 横浜市営地下鉄 GTFS 85本 平均1.9分 (路線値 75km/h) + (99343, 9934309, 9934310, 56.0), + // 京都市営地下鉄烏丸線 国際会館〜松ヶ崎: 京都市営地下鉄 GTFS 127本 平均2.2分 (路線値 75km/h) + (99610, 9961001, 9961002, 58.0), + // 京都市営地下鉄烏丸線 松ヶ崎〜北山: 京都市営地下鉄 GTFS 127本 平均1.4分 (路線値 75km/h) + (99610, 9961002, 9961003, 101.0), + // 京都市営地下鉄烏丸線 北山〜北大路: 京都市営地下鉄 GTFS 127本 平均1.4分 (路線値 75km/h) + (99610, 9961003, 9961004, 102.0), + // 京都市営地下鉄烏丸線 北大路〜鞍馬口: 京都市営地下鉄 GTFS 127本 平均1.3分 (路線値 75km/h) + (99610, 9961004, 9961005, 94.0), + // 京都市営地下鉄烏丸線 鞍馬口〜今出川: 京都市営地下鉄 GTFS 127本 平均1.2分 (路線値 75km/h) + (99610, 9961005, 9961006, 92.0), + // 京都市営地下鉄烏丸線 今出川〜丸太町: 京都市営地下鉄 GTFS 127本 平均1.9分 (路線値 75km/h) + (99610, 9961006, 9961007, 79.0), + // 京都市営地下鉄烏丸線 丸太町〜烏丸御池: 京都市営地下鉄 GTFS 127本 平均1.1分 (路線値 75km/h) + (99610, 9961007, 9961008, 84.0), + // 京都市営地下鉄烏丸線 烏丸御池〜四条: 京都市営地下鉄 GTFS 127本 平均1.3分 (路線値 75km/h) + (99610, 9961008, 9961009, 97.0), + // 京都市営地下鉄烏丸線 四条〜五条: 京都市営地下鉄 GTFS 127本 平均1.2分 (路線値 75km/h) + (99610, 9961009, 9961010, 91.0), + // 京都市営地下鉄烏丸線 五条〜京都: 京都市営地下鉄 GTFS 127本 平均1.3分 (路線値 75km/h) + (99610, 9961010, 9961011, 99.0), + // 京都市営地下鉄烏丸線 京都〜九条: 京都市営地下鉄 GTFS 127本 平均1.3分 (路線値 75km/h) + (99610, 9961011, 9961012, 95.0), + // 京都市営地下鉄烏丸線 九条〜十条: 京都市営地下鉄 GTFS 127本 平均1.2分 (路線値 75km/h) + (99610, 9961012, 9961013, 90.0), + // 京都市営地下鉄烏丸線 十条〜くいな橋: 京都市営地下鉄 GTFS 127本 平均1.4分 (路線値 75km/h) + (99610, 9961013, 9961014, 106.0), + // 京都市営地下鉄烏丸線 くいな橋〜竹田: 京都市営地下鉄 GTFS 127本 平均1.1分 (路線値 75km/h) + (99610, 9961014, 9961015, 83.0), + // 京都市営地下鉄東西線 六地蔵〜石田: 京都市営地下鉄 GTFS 112本 平均1.6分 (路線値 65km/h) + (99611, 9961101, 9961102, 58.0), + // 京都市営地下鉄東西線 醍醐〜小野: 京都市営地下鉄 GTFS 112本 平均1.6分 (路線値 65km/h) + (99611, 9961103, 9961104, 71.0), + // 京都市営地下鉄東西線 小野〜椥辻: 京都市営地下鉄 GTFS 112本 平均1.7分 (路線値 65km/h) + (99611, 9961104, 9961105, 74.0), + // 京都市営地下鉄東西線 椥辻〜東野: 京都市営地下鉄 GTFS 112本 平均1.4分 (路線値 65km/h) + (99611, 9961105, 9961106, 104.0), + // 京都市営地下鉄東西線 東野〜山科: 京都市営地下鉄 GTFS 112本 平均1.4分 (路線値 65km/h) + (99611, 9961106, 9961107, 105.0), + // 京都市営地下鉄東西線 山科〜御陵: 京都市営地下鉄 GTFS 112本 平均2.0分 (路線値 65km/h) + (99611, 9961107, 9961108, 70.0), + // 京都市営地下鉄東西線 御陵〜蹴上: 京都市営地下鉄 GTFS 154本 平均2.1分 (路線値 65km/h) + (99611, 9961108, 9961109, 76.0), + // 京都市営地下鉄東西線 東山〜三条京阪: 京都市営地下鉄 GTFS 154本 平均1.1分 (路線値 65km/h) + (99611, 9961110, 9961111, 79.0), + // 京都市営地下鉄東西線 三条京阪〜京都市役所前: 京都市営地下鉄 GTFS 154本 平均1.0分 (路線値 65km/h) + (99611, 9961111, 9961112, 71.0), + // 京都市営地下鉄東西線 京都市役所前〜烏丸御池: 京都市営地下鉄 GTFS 154本 平均1.3分 (路線値 65km/h) + (99611, 9961112, 9961113, 93.0), + // 京都市営地下鉄東西線 烏丸御池〜二条城前: 京都市営地下鉄 GTFS 154本 平均1.3分 (路線値 65km/h) + (99611, 9961113, 9961114, 94.0), + // 京都市営地下鉄東西線 二条城前〜二条: 京都市営地下鉄 GTFS 154本 平均1.2分 (路線値 65km/h) + (99611, 9961114, 9961115, 91.0), + // 京都市営地下鉄東西線 二条〜西大路御池: 京都市営地下鉄 GTFS 154本 平均1.4分 (路線値 65km/h) + (99611, 9961115, 9961116, 100.0), +]; + +/// [`crate::domain::speed_table::line_speed_override_kmh`] の凍結版。 +pub fn legacy_line_speed_override_kmh(line_cd: i32, kind: Option) -> Option { + let kind = TrainTypeKind::try_from(kind.unwrap_or(0)).ok()?; + LEGACY_LINE_SPEED_OVERRIDES + .iter() + .chain(LEGACY_LINE_SPEED_OVERRIDES_GTFS.iter()) + .find(|(lc, k, _)| *lc == line_cd && *k == kind) + .map(|(_, _, v)| *v) +} + +/// [`crate::domain::segment_speed_table::segment_speed_override_kmh`] の凍結版。 +/// 適用してよい種別の判定は同じ規則 ([`segment_override_applies_to_kind`]) を使う。 +pub fn legacy_segment_speed_override_kmh( + line_cd: i32, + station_cd_a: i32, + station_cd_b: i32, + kind: Option, +) -> Option { + if !segment_override_applies_to_kind(kind) { + return None; + } + let (lo, hi) = ( + station_cd_a.min(station_cd_b), + station_cd_a.max(station_cd_b), + ); + LEGACY_SEGMENT_SPEED_OVERRIDES + .binary_search_by(|&(lc, a, b, _)| (lc, a, b).cmp(&(line_cd, lo, hi))) + .ok() + .map(|i| LEGACY_SEGMENT_SPEED_OVERRIDES[i].3) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn segment_table_is_sorted_for_binary_search() { + assert!(LEGACY_SEGMENT_SPEED_OVERRIDES + .windows(2) + .all(|w| (w[0].0, w[0].1, w[0].2) < (w[1].0, w[1].1, w[1].2))); + } + + #[test] + fn frozen_values_are_kept() { + // 凍結時点の手動較正と GTFS 較正が引けること (オートモードの速度を固定する) + assert_eq!( + legacy_line_speed_override_kmh(23006, Some(TrainTypeKind::LimitedExpress as i32)), + Some(160.0) + ); + assert_eq!( + legacy_line_speed_override_kmh(27001, Some(TrainTypeKind::Express as i32)), + Some(120.0) + ); + assert_eq!(legacy_line_speed_override_kmh(28001, None), Some(55.0)); + // 優等種別には駅間別較正を適用しない + let (lc, a, b, v) = LEGACY_SEGMENT_SPEED_OVERRIDES[0]; + assert_eq!(legacy_segment_speed_override_kmh(lc, b, a, None), Some(v)); + assert_eq!( + legacy_segment_speed_override_kmh(lc, a, b, Some(TrainTypeKind::Express as i32)), + None + ); + } +} diff --git a/stationapi/src/domain/route_search.rs b/stationapi/src/domain/route_search.rs index f7c80dd1..55383db4 100644 --- a/stationapi/src/domain/route_search.rs +++ b/stationapi/src/domain/route_search.rs @@ -27,7 +27,8 @@ use std::collections::{HashMap, HashSet, VecDeque}; use crate::domain::arrival_estimation::{ - estimate_arrival_minutes_calibrated, is_circular_route, EstimationParams, + estimate_arrival_minutes_with_track, is_circular_route, track_distance_key, EstimationParams, + TrackDistances, }; use crate::domain::entity::station::Station; use crate::domain::route_topology::{trim_pattern, RouteStop, RouteTopology}; @@ -191,10 +192,21 @@ impl RouteNetwork { pub fn build(line_groups: I, params: &EstimationParams) -> Self where I: IntoIterator>, + { + Self::build_with_track(line_groups, params, |_, _| None) + } + + /// [`RouteNetwork::build`] に、隣り合う 2 駅の線路の長さ (m) を引く関数を渡す版。 + /// 乗車の所要時間は `estimateArrivalTimes` と同じく、線路の長さがある駅間では + /// それを使って推定する (到着の早い順の並べ替えを見込みと一致させるため)。 + pub fn build_with_track(line_groups: I, params: &EstimationParams, track: F) -> Self + where + I: IntoIterator>, + F: Fn(i32, i32) -> Option, { let mut network = RouteNetwork::default(); for stations in line_groups { - network.add_pattern(stations, params); + network.add_pattern(stations, params, &track); } network.topology.finish(); network @@ -219,7 +231,12 @@ impl RouteNetwork { node } - fn add_pattern(&mut self, stations: Vec, params: &EstimationParams) { + fn add_pattern( + &mut self, + stations: Vec, + params: &EstimationParams, + track: &dyn Fn(i32, i32) -> Option, + ) { let mut stations: Vec = stations .into_iter() .filter(|s| s.sst_id.is_some() && s.line_group_cd.is_some()) @@ -248,15 +265,23 @@ impl RouteNetwork { let refs: Vec<&Station> = stations.iter().collect(); let circular = is_circular_route(&refs); + // 系統の隣り合う駅の組 (環状線は継ぎ目の組も) の線路の長さ + let mut distances = TrackDistances::new(); + let seam = circular.then(|| (refs[refs.len() - 1], refs[0])); + for (a, b) in refs.windows(2).map(|w| (w[0], w[1])).chain(seam) { + if let Some(d) = track(a.station_cd, b.station_cd) { + distances.insert(track_distance_key(a.station_cd, b.station_cd), d); + } + } let estimated = if circular { // 二周ぶん + 始点に戻る 1 駅を並べ、継ぎ目を跨ぐ乗車も同じ配列で引く。 let mut unrolled: Vec<&Station> = Vec::with_capacity(refs.len() * 2 + 1); unrolled.extend(refs.iter().copied()); unrolled.extend(refs.iter().copied()); unrolled.push(refs[0]); - estimate_arrival_minutes_calibrated(&unrolled, &refs, params) + estimate_arrival_minutes_with_track(&unrolled, &refs, params, Some(&distances)) } else { - estimate_arrival_minutes_calibrated(&refs, &refs, params) + estimate_arrival_minutes_with_track(&refs, &refs, params, Some(&distances)) }; let to_seconds = |minutes: f64| -> i32 { if minutes.is_finite() { @@ -908,6 +933,29 @@ mod tests { assert_eq!(forward[0].total_seconds, backward[0].total_seconds); } + #[test] + fn ride_times_use_track_distances_when_given() { + let line = || { + [straight( + 100, + &[(1, 0.0, 0.0), (2, 1.0, 0.0), (3, 2.0, 0.0), (4, 3.0, 0.0)], + )] + }; + let plain = RouteNetwork::build(line(), &EstimationParams::default()); + // 線路の長さを直線より長く渡すと、乗車の所要時間が延びる + let tracked = + RouteNetwork::build_with_track(line(), &EstimationParams::default(), |a, b| { + let straight_m = 111_000.0 * f64::from((a - b).abs()); + Some(straight_m * 1.5) + }); + let plain_seconds = plain.search(1, 4, None)[0].total_seconds; + let tracked_seconds = tracked.search(1, 4, None)[0].total_seconds; + assert!( + tracked_seconds > plain_seconds, + "{tracked_seconds} > {plain_seconds}" + ); + } + #[test] fn transfers_at_shared_station_group() { let network = t_shaped(); diff --git a/stationapi/src/domain/segment_speed_table.rs b/stationapi/src/domain/segment_speed_table.rs index 0d1f23a2..b917e993 100644 --- a/stationapi/src/domain/segment_speed_table.rs +++ b/stationapi/src/domain/segment_speed_table.rs @@ -25,147 +25,151 @@ use crate::model::TrainTypeKind; /// `(line_cd, station_cd_小, station_cd_大)` の昇順にソート済み(二分探索で引く)。 const SEGMENT_SPEED_OVERRIDES: &[(i32, i32, i32, f64)] = &[ // --- BEGIN GENERATED (scripts/compute_speed_table.py) --- - // 東京メトロ銀座線 浅草〜田原町: 東京メトロ GTFS 218本 平均1.5分 (路線値 55km/h) - (28001, 2800101, 2800102, 34.0), - // 東京メトロ銀座線 田原町〜稲荷町: 東京メトロ GTFS 218本 平均1.2分 (路線値 55km/h) - (28001, 2800102, 2800103, 74.0), - // 東京メトロ銀座線 稲荷町〜上野: 東京メトロ GTFS 218本 平均1.3分 (路線値 55km/h) - (28001, 2800103, 2800104, 31.0), - // 東京メトロ銀座線 上野〜上野広小路: 東京メトロ GTFS 224本 平均1.4分 (路線値 55km/h) - (28001, 2800104, 2800105, 36.0), - // 東京メトロ銀座線 上野広小路〜末広町: 東京メトロ GTFS 224本 平均1.0分 (路線値 55km/h) - (28001, 2800105, 2800106, 75.0), - // 東京メトロ銀座線 末広町〜神田: 東京メトロ GTFS 224本 平均1.4分 (路線値 55km/h) - (28001, 2800106, 2800107, 105.0), - // 東京メトロ銀座線 神田〜三越前: 東京メトロ GTFS 224本 平均1.2分 (路線値 55km/h) - (28001, 2800107, 2800108, 90.0), - // 東京メトロ銀座線 三越前〜日本橋: 東京メトロ GTFS 224本 平均1.0分 (路線値 55km/h) - (28001, 2800108, 2800109, 77.0), - // 東京メトロ銀座線 日本橋〜京橋: 東京メトロ GTFS 224本 平均1.1分 (路線値 55km/h) - (28001, 2800109, 2800110, 83.0), - // 東京メトロ銀座線 京橋〜銀座: 東京メトロ GTFS 224本 平均1.2分 (路線値 55km/h) - (28001, 2800110, 2800111, 90.0), - // 東京メトロ銀座線 銀座〜新橋: 東京メトロ GTFS 224本 平均1.2分 (路線値 55km/h) - (28001, 2800111, 2800112, 87.0), - // 東京メトロ銀座線 新橋〜虎ノ門: 東京メトロ GTFS 224本 平均1.3分 (路線値 55km/h) - (28001, 2800112, 2800113, 94.0), - // 東京メトロ銀座線 虎ノ門〜溜池山王: 東京メトロ GTFS 224本 平均1.3分 (路線値 55km/h) - (28001, 2800113, 2800114, 95.0), - // 東京メトロ銀座線 溜池山王〜赤坂見附: 東京メトロ GTFS 224本 平均1.0分 (路線値 55km/h) + // 東京メトロ銀座線 浅草〜田原町: 東京メトロ GTFS 280本 平均1.6分 (路線値 55km/h) + (28001, 2800101, 2800102, 37.0), + // 東京メトロ銀座線 田原町〜稲荷町: 東京メトロ GTFS 280本 平均1.2分 (路線値 55km/h) + (28001, 2800102, 2800103, 88.0), + // 東京メトロ銀座線 稲荷町〜上野: 東京メトロ GTFS 280本 平均1.0分 (路線値 55km/h) + (28001, 2800103, 2800104, 73.0), + // 東京メトロ銀座線 上野〜上野広小路: 東京メトロ GTFS 282本 平均1.3分 (路線値 55km/h) + (28001, 2800104, 2800105, 45.0), + // 東京メトロ銀座線 上野広小路〜末広町: 東京メトロ GTFS 282本 平均1.0分 (路線値 55km/h) + (28001, 2800105, 2800106, 74.0), + // 東京メトロ銀座線 末広町〜神田: 東京メトロ GTFS 282本 平均1.4分 (路線値 55km/h) + (28001, 2800106, 2800107, 104.0), + // 東京メトロ銀座線 神田〜三越前: 東京メトロ GTFS 282本 平均1.2分 (路線値 55km/h) + (28001, 2800107, 2800108, 88.0), + // 東京メトロ銀座線 三越前〜日本橋: 東京メトロ GTFS 282本 平均1.1分 (路線値 55km/h) + (28001, 2800108, 2800109, 78.0), + // 東京メトロ銀座線 日本橋〜京橋: 東京メトロ GTFS 282本 平均1.1分 (路線値 55km/h) + (28001, 2800109, 2800110, 82.0), + // 東京メトロ銀座線 京橋〜銀座: 東京メトロ GTFS 282本 平均1.3分 (路線値 55km/h) + (28001, 2800110, 2800111, 96.0), + // 東京メトロ銀座線 銀座〜新橋: 東京メトロ GTFS 282本 平均1.3分 (路線値 55km/h) + (28001, 2800111, 2800112, 98.0), + // 東京メトロ銀座線 新橋〜虎ノ門: 東京メトロ GTFS 282本 平均1.2分 (路線値 55km/h) + (28001, 2800112, 2800113, 92.0), + // 東京メトロ銀座線 虎ノ門〜溜池山王: 東京メトロ GTFS 282本 平均1.3分 (路線値 55km/h) + (28001, 2800113, 2800114, 93.0), + // 東京メトロ銀座線 溜池山王〜赤坂見附: 東京メトロ GTFS 282本 平均1.0分 (路線値 55km/h) (28001, 2800114, 2800115, 76.0), // 東京メトロ銀座線 赤坂見附〜青山一丁目: 東京メトロ GTFS 224本 平均2.2分 (路線値 55km/h) (28001, 2800115, 2800116, 48.0), - // 東京メトロ銀座線 青山一丁目〜外苑前: 東京メトロ GTFS 224本 平均1.1分 (路線値 55km/h) - (28001, 2800116, 2800117, 81.0), - // 東京メトロ銀座線 外苑前〜表参道: 東京メトロ GTFS 224本 平均1.2分 (路線値 55km/h) - (28001, 2800117, 2800118, 90.0), + // 東京メトロ銀座線 青山一丁目〜外苑前: 東京メトロ GTFS 282本 平均1.1分 (路線値 55km/h) + (28001, 2800116, 2800117, 80.0), + // 東京メトロ銀座線 外苑前〜表参道: 東京メトロ GTFS 282本 平均1.2分 (路線値 55km/h) + (28001, 2800117, 2800118, 89.0), // 東京メトロ銀座線 表参道〜渋谷: 東京メトロ GTFS 224本 平均1.6分 (路線値 55km/h) (28001, 2800118, 2800119, 94.0), - // 東京メトロ丸ノ内線 茗荷谷〜後楽園: 東京メトロ GTFS 149本 平均2.2分 (路線値 65km/h) - (28002, 2800203, 2800204, 74.0), - // 東京メトロ丸ノ内線 後楽園〜本郷三丁目: 東京メトロ GTFS 149本 平均1.2分 (路線値 65km/h) - (28002, 2800204, 2800205, 91.0), - // 東京メトロ丸ノ内線 本郷三丁目〜御茶ノ水: 東京メトロ GTFS 149本 平均1.3分 (路線値 65km/h) - (28002, 2800205, 2800206, 93.0), - // 東京メトロ丸ノ内線 御茶ノ水〜淡路町: 東京メトロ GTFS 149本 平均1.2分 (路線値 65km/h) - (28002, 2800206, 2800207, 87.0), - // 東京メトロ丸ノ内線 淡路町〜大手町: 東京メトロ GTFS 149本 平均1.4分 (路線値 65km/h) - (28002, 2800207, 2800208, 105.0), - // 東京メトロ丸ノ内線 大手町〜東京: 東京メトロ GTFS 149本 平均1.1分 (路線値 65km/h) - (28002, 2800208, 2800209, 79.0), - // 東京メトロ丸ノ内線 東京〜銀座: 東京メトロ GTFS 149本 平均2.2分 (路線値 65km/h) - (28002, 2800209, 2800210, 44.0), - // 東京メトロ丸ノ内線 霞ケ関〜国会議事堂前: 東京メトロ GTFS 149本 平均1.0分 (路線値 65km/h) - (28002, 2800211, 2800212, 76.0), - // 東京メトロ丸ノ内線 国会議事堂前〜赤坂見附: 東京メトロ GTFS 149本 平均1.3分 (路線値 65km/h) - (28002, 2800212, 2800213, 75.0), - // 東京メトロ丸ノ内線 赤坂見附〜四ツ谷: 東京メトロ GTFS 149本 平均2.3分 (路線値 65km/h) - (28002, 2800213, 2800214, 40.0), - // 東京メトロ丸ノ内線 四ツ谷〜四谷三丁目: 東京メトロ GTFS 149本 平均1.4分 (路線値 65km/h) - (28002, 2800214, 2800215, 104.0), - // 東京メトロ丸ノ内線 四谷三丁目〜新宿御苑前: 東京メトロ GTFS 149本 平均1.3分 (路線値 65km/h) - (28002, 2800215, 2800216, 82.0), - // 東京メトロ丸ノ内線 新宿御苑前〜新宿三丁目: 東京メトロ GTFS 149本 平均1.1分 (路線値 65km/h) - (28002, 2800216, 2800217, 81.0), - // 東京メトロ丸ノ内線 新宿〜西新宿: 東京メトロ GTFS 149本 平均1.2分 (路線値 65km/h) - (28002, 2800218, 2800219, 92.0), - // 東京メトロ丸ノ内線 西新宿〜中野坂上: 東京メトロ GTFS 149本 平均1.4分 (路線値 65km/h) - (28002, 2800219, 2800220, 105.0), + // 東京メトロ丸ノ内線 池袋〜新大塚: 東京メトロ GTFS 159本 平均2.5分 (路線値 60km/h) + (28002, 2800201, 2800202, 66.0), + // 東京メトロ丸ノ内線 茗荷谷〜後楽園: 東京メトロ GTFS 159本 平均2.2分 (路線値 60km/h) + (28002, 2800203, 2800204, 70.0), + // 東京メトロ丸ノ内線 後楽園〜本郷三丁目: 東京メトロ GTFS 159本 平均1.2分 (路線値 60km/h) + (28002, 2800204, 2800205, 88.0), + // 東京メトロ丸ノ内線 本郷三丁目〜御茶ノ水: 東京メトロ GTFS 159本 平均1.2分 (路線値 60km/h) + (28002, 2800205, 2800206, 92.0), + // 東京メトロ丸ノ内線 御茶ノ水〜淡路町: 東京メトロ GTFS 159本 平均1.2分 (路線値 60km/h) + (28002, 2800206, 2800207, 83.0), + // 東京メトロ丸ノ内線 淡路町〜大手町: 東京メトロ GTFS 159本 平均1.4分 (路線値 60km/h) + (28002, 2800207, 2800208, 91.0), + // 東京メトロ丸ノ内線 大手町〜東京: 東京メトロ GTFS 159本 平均1.0分 (路線値 60km/h) + (28002, 2800208, 2800209, 75.0), + // 東京メトロ丸ノ内線 東京〜銀座: 東京メトロ GTFS 159本 平均1.6分 (路線値 60km/h) + (28002, 2800209, 2800210, 105.0), + // 東京メトロ丸ノ内線 霞ケ関〜国会議事堂前: 東京メトロ GTFS 159本 平均1.1分 (路線値 60km/h) + (28002, 2800211, 2800212, 56.0), + // 東京メトロ丸ノ内線 国会議事堂前〜赤坂見附: 東京メトロ GTFS 159本 平均1.4分 (路線値 60km/h) + (28002, 2800212, 2800213, 80.0), + // 東京メトロ丸ノ内線 赤坂見附〜四ツ谷: 東京メトロ GTFS 159本 平均2.3分 (路線値 60km/h) + (28002, 2800213, 2800214, 41.0), + // 東京メトロ丸ノ内線 四ツ谷〜四谷三丁目: 東京メトロ GTFS 159本 平均1.4分 (路線値 60km/h) + (28002, 2800214, 2800215, 95.0), + // 東京メトロ丸ノ内線 四谷三丁目〜新宿御苑前: 東京メトロ GTFS 159本 平均1.4分 (路線値 60km/h) + (28002, 2800215, 2800216, 66.0), + // 東京メトロ丸ノ内線 新宿御苑前〜新宿三丁目: 東京メトロ GTFS 159本 平均1.0分 (路線値 60km/h) + (28002, 2800216, 2800217, 77.0), + // 東京メトロ丸ノ内線 新宿三丁目〜新宿: 東京メトロ GTFS 159本 平均0.9分 (路線値 60km/h) + (28002, 2800217, 2800218, 65.0), + // 東京メトロ丸ノ内線 新宿〜西新宿: 東京メトロ GTFS 159本 平均1.2分 (路線値 60km/h) + (28002, 2800218, 2800219, 89.0), + // 東京メトロ丸ノ内線 西新宿〜中野坂上: 東京メトロ GTFS 159本 平均1.4分 (路線値 60km/h) + (28002, 2800219, 2800220, 100.0), // 東京メトロ丸ノ内線 中野坂上〜新中野: 東京メトロ GTFS 149本 平均1.7分 (路線値 65km/h) (28002, 2800220, 2800221, 83.0), - // 東京メトロ丸ノ内線 新中野〜東高円寺: 東京メトロ GTFS 149本 平均1.4分 (路線値 65km/h) - (28002, 2800221, 2800222, 107.0), - // 東京メトロ丸ノ内線 東高円寺〜新高円寺: 東京メトロ GTFS 149本 平均1.3分 (路線値 65km/h) - (28002, 2800222, 2800223, 99.0), + // 東京メトロ丸ノ内線 新中野〜東高円寺: 東京メトロ GTFS 159本 平均1.4分 (路線値 60km/h) + (28002, 2800221, 2800222, 102.0), + // 東京メトロ丸ノ内線 東高円寺〜新高円寺: 東京メトロ GTFS 159本 平均1.3分 (路線値 60km/h) + (28002, 2800222, 2800223, 95.0), + // 東京メトロ丸ノ内線 新高円寺〜南阿佐ケ谷: 東京メトロ GTFS 159本 平均1.5分 (路線値 60km/h) + (28002, 2800223, 2800224, 109.0), // 東京メトロ丸ノ内線 南阿佐ケ谷〜荻窪: 東京メトロ GTFS 149本 平均2.0分 (路線値 65km/h) (28002, 2800224, 2800225, 76.0), - // 東京メトロ日比谷線 北千住〜南千住: 東京メトロ GTFS 177本 平均2.3分 (路線値 50km/h) - (28003, 2800301, 2800302, 68.0), - // 東京メトロ日比谷線 入谷〜上野: 東京メトロ GTFS 184本 平均1.5分 (路線値 50km/h) - (28003, 2800304, 2800305, 109.0), - // 東京メトロ日比谷線 上野〜仲御徒町: 東京メトロ GTFS 184本 平均1.0分 (路線値 50km/h) - (28003, 2800305, 2800306, 76.0), - // 東京メトロ日比谷線 仲御徒町〜秋葉原: 東京メトロ GTFS 184本 平均1.4分 (路線値 50km/h) - (28003, 2800306, 2800307, 100.0), - // 東京メトロ日比谷線 秋葉原〜小伝馬町: 東京メトロ GTFS 184本 平均1.3分 (路線値 50km/h) - (28003, 2800307, 2800308, 96.0), - // 東京メトロ日比谷線 小伝馬町〜人形町: 東京メトロ GTFS 184本 平均1.1分 (路線値 50km/h) - (28003, 2800308, 2800309, 80.0), - // 東京メトロ日比谷線 人形町〜茅場町: 東京メトロ GTFS 184本 平均1.2分 (路線値 50km/h) - (28003, 2800309, 2800310, 90.0), - // 東京メトロ日比谷線 茅場町〜八丁堀: 東京メトロ GTFS 184本 平均1.1分 (路線値 50km/h) - (28003, 2800310, 2800311, 80.0), - // 東京メトロ日比谷線 八丁堀〜築地: 東京メトロ GTFS 184本 平均1.3分 (路線値 50km/h) - (28003, 2800311, 2800312, 95.0), - // 東京メトロ日比谷線 築地〜東銀座: 東京メトロ GTFS 184本 平均1.0分 (路線値 50km/h) - (28003, 2800312, 2800313, 73.0), - // 東京メトロ日比谷線 東銀座〜銀座: 東京メトロ GTFS 184本 平均1.1分 (路線値 50km/h) - (28003, 2800313, 2800314, 35.0), - // 東京メトロ日比谷線 銀座〜日比谷: 東京メトロ GTFS 184本 平均0.9分 (路線値 50km/h) - (28003, 2800314, 2800315, 69.0), - // 東京メトロ日比谷線 日比谷〜霞ケ関: 東京メトロ GTFS 184本 平均1.7分 (路線値 50km/h) - (28003, 2800315, 2800316, 43.0), - // 東京メトロ日比谷線 霞ケ関〜虎ノ門ヒルズ: 東京メトロ GTFS 184本 平均1.2分 (路線値 50km/h) - (28003, 2800316, 2800322, 90.0), + // 東京メトロ日比谷線 北千住〜南千住: 東京メトロ GTFS 177本 平均2.3分 (路線値 55km/h) + (28003, 2800301, 2800302, 64.0), + // 東京メトロ日比谷線 入谷〜上野: 東京メトロ GTFS 184本 平均1.4分 (路線値 55km/h) + (28003, 2800304, 2800305, 107.0), + // 東京メトロ日比谷線 上野〜仲御徒町: 東京メトロ GTFS 184本 平均1.0分 (路線値 55km/h) + (28003, 2800305, 2800306, 75.0), + // 東京メトロ日比谷線 仲御徒町〜秋葉原: 東京メトロ GTFS 184本 平均1.3分 (路線値 55km/h) + (28003, 2800306, 2800307, 98.0), + // 東京メトロ日比谷線 秋葉原〜小伝馬町: 東京メトロ GTFS 184本 平均1.3分 (路線値 55km/h) + (28003, 2800307, 2800308, 95.0), + // 東京メトロ日比谷線 小伝馬町〜人形町: 東京メトロ GTFS 184本 平均1.1分 (路線値 55km/h) + (28003, 2800308, 2800309, 78.0), + // 東京メトロ日比谷線 人形町〜茅場町: 東京メトロ GTFS 184本 平均1.2分 (路線値 55km/h) + (28003, 2800309, 2800310, 92.0), + // 東京メトロ日比谷線 茅場町〜八丁堀: 東京メトロ GTFS 184本 平均1.1分 (路線値 55km/h) + (28003, 2800310, 2800311, 79.0), + // 東京メトロ日比谷線 八丁堀〜築地: 東京メトロ GTFS 184本 平均1.3分 (路線値 55km/h) + (28003, 2800311, 2800312, 94.0), + // 東京メトロ日比谷線 築地〜東銀座: 東京メトロ GTFS 184本 平均1.1分 (路線値 55km/h) + (28003, 2800312, 2800313, 81.0), + // 東京メトロ日比谷線 東銀座〜銀座: 東京メトロ GTFS 184本 平均1.1分 (路線値 55km/h) + (28003, 2800313, 2800314, 31.0), + // 東京メトロ日比谷線 銀座〜日比谷: 東京メトロ GTFS 184本 平均0.9分 (路線値 55km/h) + (28003, 2800314, 2800315, 68.0), + // 東京メトロ日比谷線 霞ケ関〜虎ノ門ヒルズ: 東京メトロ GTFS 184本 平均1.2分 (路線値 55km/h) + (28003, 2800316, 2800322, 88.0), // 東京メトロ日比谷線 神谷町〜六本木: 東京メトロ GTFS 184本 平均1.6分 (路線値 50km/h) (28003, 2800317, 2800318, 83.0), - // 東京メトロ日比谷線 神谷町〜虎ノ門ヒルズ: 東京メトロ GTFS 184本 平均1.0分 (路線値 50km/h) - (28003, 2800317, 2800322, 77.0), - // 東京メトロ日比谷線 広尾〜恵比寿: 東京メトロ GTFS 184本 平均2.4分 (路線値 50km/h) - (28003, 2800319, 2800320, 45.0), - // 東京メトロ日比谷線 恵比寿〜中目黒: 東京メトロ GTFS 184本 平均2.0分 (路線値 50km/h) - (28003, 2800320, 2800321, 43.0), + // 東京メトロ日比谷線 神谷町〜虎ノ門ヒルズ: 東京メトロ GTFS 184本 平均1.0分 (路線値 55km/h) + (28003, 2800317, 2800322, 76.0), + // 東京メトロ日比谷線 広尾〜恵比寿: 東京メトロ GTFS 184本 平均2.4分 (路線値 55km/h) + (28003, 2800319, 2800320, 52.0), + // 東京メトロ日比谷線 恵比寿〜中目黒: 東京メトロ GTFS 184本 平均2.0分 (路線値 55km/h) + (28003, 2800320, 2800321, 46.0), // 東京メトロ東西線 中野〜落合: 東京メトロ GTFS 126本 平均2.5分 (路線値 60km/h) - (28004, 2800401, 2800402, 71.0), + (28004, 2800401, 2800402, 72.0), // 東京メトロ東西線 落合〜高田馬場: 東京メトロ GTFS 126本 平均2.5分 (路線値 60km/h) - (28004, 2800402, 2800403, 51.0), + (28004, 2800402, 2800403, 53.0), // 東京メトロ東西線 早稲田〜神楽坂: 東京メトロ GTFS 126本 平均1.5分 (路線値 60km/h) (28004, 2800404, 2800405, 95.0), // 東京メトロ東西線 神楽坂〜飯田橋: 東京メトロ GTFS 126本 平均2.1分 (路線値 60km/h) - (28004, 2800405, 2800406, 41.0), + (28004, 2800405, 2800406, 42.0), // 東京メトロ東西線 飯田橋〜九段下: 東京メトロ GTFS 126本 平均1.4分 (路線値 60km/h) - (28004, 2800406, 2800407, 68.0), + (28004, 2800406, 2800407, 75.0), // 東京メトロ東西線 九段下〜竹橋: 東京メトロ GTFS 126本 平均1.4分 (路線値 60km/h) - (28004, 2800407, 2800408, 45.0), - // 東京メトロ東西線 竹橋〜大手町: 東京メトロ GTFS 126本 平均1.4分 (路線値 60km/h) - (28004, 2800408, 2800409, 105.0), - // 東京メトロ東西線 大手町〜日本橋: 東京メトロ GTFS 126本 平均1.3分 (路線値 60km/h) - (28004, 2800409, 2800410, 57.0), - // 東京メトロ東西線 日本橋〜茅場町: 東京メトロ GTFS 126本 平均1.1分 (路線値 60km/h) - (28004, 2800410, 2800411, 81.0), - // 東京メトロ東西線 茅場町〜門前仲町: 東京メトロ GTFS 126本 平均2.0分 (路線値 60km/h) - (28004, 2800411, 2800412, 82.0), + (28004, 2800407, 2800408, 53.0), + // 東京メトロ東西線 竹橋〜大手町: 東京メトロ GTFS 126本 平均1.5分 (路線値 60km/h) + (28004, 2800408, 2800409, 110.0), + // 東京メトロ東西線 大手町〜日本橋: 東京メトロ GTFS 126本 平均1.2分 (路線値 60km/h) + (28004, 2800409, 2800410, 79.0), + // 東京メトロ東西線 日本橋〜茅場町: 東京メトロ GTFS 126本 平均1.2分 (路線値 60km/h) + (28004, 2800410, 2800411, 86.0), + // 東京メトロ東西線 茅場町〜門前仲町: 東京メトロ GTFS 126本 平均1.9分 (路線値 60km/h) + (28004, 2800411, 2800412, 91.0), // 東京メトロ東西線 門前仲町〜木場: 東京メトロ GTFS 126本 平均1.4分 (路線値 60km/h) (28004, 2800412, 2800413, 103.0), // 東京メトロ東西線 木場〜東陽町: 東京メトロ GTFS 126本 平均1.3分 (路線値 60km/h) (28004, 2800413, 2800414, 99.0), // 東京メトロ東西線 南砂町〜西葛西: 東京メトロ GTFS 117本 平均2.4分 (路線値 60km/h) - (28004, 2800415, 2800416, 106.0), + (28004, 2800415, 2800416, 105.0), // 東京メトロ東西線 西葛西〜葛西: 東京メトロ GTFS 117本 平均1.6分 (路線値 60km/h) - (28004, 2800416, 2800417, 81.0), + (28004, 2800416, 2800417, 79.0), // 東京メトロ東西線 葛西〜浦安: 東京メトロ GTFS 117本 平均2.5分 (路線値 60km/h) (28004, 2800417, 2800418, 66.0), - // 東京メトロ東西線 浦安〜南行徳: 東京メトロ GTFS 117本 平均1.5分 (路線値 60km/h) + // 東京メトロ東西線 浦安〜南行徳: 東京メトロ GTFS 117本 平均1.4分 (路線値 60km/h) (28004, 2800418, 2800419, 107.0), // 東京メトロ東西線 行徳〜妙典: 東京メトロ GTFS 117本 平均1.6分 (路線値 60km/h) (28004, 2800420, 2800421, 90.0), @@ -173,64 +177,66 @@ const SEGMENT_SPEED_OVERRIDES: &[(i32, i32, i32, f64)] = &[ (28004, 2800421, 2800422, 108.0), // 東京メトロ東西線 原木中山〜西船橋: 東京メトロ GTFS 113本 平均1.9分 (路線値 60km/h) (28004, 2800422, 2800423, 84.0), - // 東京メトロ千代田線 北綾瀬〜綾瀬: 東京メトロ GTFS 50本 平均4.2分 (路線値 75km/h) - (28005, 2800501, 2800502, 33.0), - // 東京メトロ千代田線 綾瀬〜北千住: 東京メトロ GTFS 176本 平均3.2分 (路線値 75km/h) - (28005, 2800502, 2800503, 66.0), - // 東京メトロ千代田線 北千住〜町屋: 東京メトロ GTFS 176本 平均2.7分 (路線値 75km/h) - (28005, 2800503, 2800504, 79.0), - // 東京メトロ千代田線 西日暮里〜千駄木: 東京メトロ GTFS 176本 平均1.3分 (路線値 75km/h) - (28005, 2800505, 2800506, 95.0), - // 東京メトロ千代田線 千駄木〜根津: 東京メトロ GTFS 176本 平均1.4分 (路線値 75km/h) - (28005, 2800506, 2800507, 102.0), - // 東京メトロ千代田線 根津〜湯島: 東京メトロ GTFS 176本 平均1.5分 (路線値 75km/h) - (28005, 2800507, 2800508, 111.0), + // 東京メトロ千代田線 北綾瀬〜綾瀬: 東京メトロ GTFS 50本 平均4.2分 (路線値 65km/h) + (28005, 2800501, 2800502, 36.0), + // 東京メトロ千代田線 北千住〜町屋: 東京メトロ GTFS 176本 平均2.7分 (路線値 65km/h) + (28005, 2800503, 2800504, 77.0), + // 東京メトロ千代田線 西日暮里〜千駄木: 東京メトロ GTFS 176本 平均1.3分 (路線値 65km/h) + (28005, 2800505, 2800506, 93.0), + // 東京メトロ千代田線 千駄木〜根津: 東京メトロ GTFS 176本 平均1.4分 (路線値 65km/h) + (28005, 2800506, 2800507, 101.0), + // 東京メトロ千代田線 根津〜湯島: 東京メトロ GTFS 176本 平均1.4分 (路線値 65km/h) + (28005, 2800507, 2800508, 107.0), // 東京メトロ千代田線 湯島〜新御茶ノ水: 東京メトロ GTFS 176本 平均1.6分 (路線値 75km/h) (28005, 2800508, 2800509, 115.0), - // 東京メトロ千代田線 大手町〜二重橋前: 東京メトロ GTFS 176本 平均1.1分 (路線値 75km/h) - (28005, 2800510, 2800511, 81.0), - // 東京メトロ千代田線 二重橋前〜日比谷: 東京メトロ GTFS 176本 平均1.3分 (路線値 75km/h) - (28005, 2800511, 2800512, 96.0), - // 東京メトロ千代田線 日比谷〜霞ケ関: 東京メトロ GTFS 176本 平均1.2分 (路線値 75km/h) - (28005, 2800512, 2800513, 89.0), - // 東京メトロ千代田線 国会議事堂前〜赤坂: 東京メトロ GTFS 176本 平均1.1分 (路線値 75km/h) - (28005, 2800514, 2800515, 84.0), + // 東京メトロ千代田線 大手町〜二重橋前: 東京メトロ GTFS 176本 平均1.0分 (路線値 65km/h) + (28005, 2800510, 2800511, 77.0), + // 東京メトロ千代田線 二重橋前〜日比谷: 東京メトロ GTFS 176本 平均1.2分 (路線値 65km/h) + (28005, 2800511, 2800512, 92.0), + // 東京メトロ千代田線 日比谷〜霞ケ関: 東京メトロ GTFS 176本 平均1.3分 (路線値 65km/h) + (28005, 2800512, 2800513, 95.0), + // 東京メトロ千代田線 霞ケ関〜国会議事堂前: 東京メトロ GTFS 176本 平均1.1分 (路線値 65km/h) + (28005, 2800513, 2800514, 79.0), + // 東京メトロ千代田線 国会議事堂前〜赤坂: 東京メトロ GTFS 176本 平均1.1分 (路線値 65km/h) + (28005, 2800514, 2800515, 80.0), // 東京メトロ千代田線 乃木坂〜表参道: 東京メトロ GTFS 176本 平均1.6分 (路線値 75km/h) (28005, 2800516, 2800517, 119.0), - // 東京メトロ千代田線 表参道〜明治神宮前〈原宿〉: 東京メトロ GTFS 176本 平均1.3分 (路線値 75km/h) - (28005, 2800517, 2800518, 98.0), + // 東京メトロ千代田線 表参道〜明治神宮前〈原宿〉: 東京メトロ GTFS 176本 平均1.3分 (路線値 65km/h) + (28005, 2800517, 2800518, 94.0), // 東京メトロ千代田線 明治神宮前〈原宿〉〜代々木公園: 東京メトロ GTFS 176本 平均1.8分 (路線値 75km/h) (28005, 2800518, 2800519, 84.0), - // 東京メトロ千代田線 代々木公園〜代々木上原: 東京メトロ GTFS 176本 平均1.3分 (路線値 75km/h) - (28005, 2800519, 2800520, 95.0), + // 東京メトロ千代田線 代々木公園〜代々木上原: 東京メトロ GTFS 176本 平均1.3分 (路線値 65km/h) + (28005, 2800519, 2800520, 93.0), // 東京メトロ有楽町線 和光市〜地下鉄成増: 東京メトロ GTFS 90本 平均2.5分 (路線値 65km/h) - (28006, 2800601, 2800602, 79.0), + (28006, 2800601, 2800602, 82.0), // 東京メトロ有楽町線 地下鉄成増〜地下鉄赤塚: 東京メトロ GTFS 90本 平均1.8分 (路線値 65km/h) (28006, 2800602, 2800603, 76.0), // 東京メトロ有楽町線 地下鉄赤塚〜平和台: 東京メトロ GTFS 90本 平均2.1分 (路線値 65km/h) - (28006, 2800603, 2800604, 71.0), + (28006, 2800603, 2800604, 82.0), // 東京メトロ有楽町線 氷川台〜小竹向原: 東京メトロ GTFS 90本 平均1.6分 (路線値 65km/h) - (28006, 2800605, 2800606, 101.0), + (28006, 2800605, 2800606, 93.0), // 東京メトロ有楽町線 小竹向原〜千川: 東京メトロ GTFS 142本 平均1.6分 (路線値 65km/h) - (28006, 2800606, 2800607, 74.0), + (28006, 2800606, 2800607, 72.0), // 東京メトロ有楽町線 千川〜要町: 東京メトロ GTFS 142本 平均1.4分 (路線値 65km/h) (28006, 2800607, 2800608, 103.0), + // 東京メトロ有楽町線 要町〜池袋: 東京メトロ GTFS 142本 平均1.6分 (路線値 65km/h) + (28006, 2800608, 2800609, 69.0), // 東京メトロ有楽町線 池袋〜東池袋: 東京メトロ GTFS 143本 平均1.7分 (路線値 65km/h) - (28006, 2800609, 2800610, 49.0), + (28006, 2800609, 2800610, 50.0), // 東京メトロ有楽町線 東池袋〜護国寺: 東京メトロ GTFS 143本 平均1.4分 (路線値 65km/h) - (28006, 2800610, 2800611, 104.0), + (28006, 2800610, 2800611, 102.0), // 東京メトロ有楽町線 江戸川橋〜飯田橋: 東京メトロ GTFS 143本 平均2.4分 (路線値 65km/h) - (28006, 2800612, 2800613, 41.0), - // 東京メトロ有楽町線 市ケ谷〜麹町: 東京メトロ GTFS 143本 平均1.3分 (路線値 65km/h) - (28006, 2800614, 2800615, 93.0), - // 東京メトロ有楽町線 麹町〜永田町: 東京メトロ GTFS 143本 平均1.2分 (路線値 65km/h) - (28006, 2800615, 2800616, 49.0), + (28006, 2800612, 2800613, 51.0), + // 東京メトロ有楽町線 市ケ谷〜麴町: 東京メトロ GTFS 143本 平均1.3分 (路線値 65km/h) + (28006, 2800614, 2800615, 94.0), + // 東京メトロ有楽町線 麴町〜永田町: 東京メトロ GTFS 143本 平均1.2分 (路線値 65km/h) + (28006, 2800615, 2800616, 48.0), // 東京メトロ有楽町線 永田町〜桜田門: 東京メトロ GTFS 143本 平均1.4分 (路線値 65km/h) - (28006, 2800616, 2800617, 94.0), + (28006, 2800616, 2800617, 105.0), // 東京メトロ有楽町線 桜田門〜有楽町: 東京メトロ GTFS 143本 平均1.4分 (路線値 65km/h) (28006, 2800617, 2800618, 106.0), // 東京メトロ有楽町線 銀座一丁目〜新富町: 東京メトロ GTFS 143本 平均1.2分 (路線値 65km/h) - (28006, 2800619, 2800620, 88.0), + (28006, 2800619, 2800620, 87.0), // 東京メトロ有楽町線 新富町〜月島: 東京メトロ GTFS 143本 平均1.5分 (路線値 65km/h) (28006, 2800620, 2800621, 109.0), // 東京メトロ有楽町線 月島〜豊洲: 東京メトロ GTFS 143本 平均1.9分 (路線値 65km/h) @@ -238,469 +244,463 @@ const SEGMENT_SPEED_OVERRIDES: &[(i32, i32, i32, f64)] = &[ // 東京メトロ有楽町線 豊洲〜辰巳: 東京メトロ GTFS 143本 平均1.9分 (路線値 65km/h) (28006, 2800622, 2800623, 85.0), // 東京メトロ半蔵門線 渋谷〜表参道: 東京メトロ GTFS 184本 平均1.7分 (路線値 55km/h) - (28008, 2800801, 2800802, 85.0), + (28008, 2800801, 2800802, 77.0), // 東京メトロ半蔵門線 表参道〜青山一丁目: 東京メトロ GTFS 184本 平均1.7分 (路線値 55km/h) - (28008, 2800802, 2800803, 107.0), + (28008, 2800802, 2800803, 97.0), // 東京メトロ半蔵門線 青山一丁目〜永田町: 東京メトロ GTFS 184本 平均1.9分 (路線値 55km/h) (28008, 2800803, 2800804, 95.0), - // 東京メトロ半蔵門線 永田町〜半蔵門: 東京メトロ GTFS 184本 平均1.3分 (路線値 55km/h) - (28008, 2800804, 2800805, 74.0), + // 東京メトロ半蔵門線 永田町〜半蔵門: 東京メトロ GTFS 184本 平均1.4分 (路線値 55km/h) + (28008, 2800804, 2800805, 68.0), + // 東京メトロ半蔵門線 半蔵門〜九段下: 東京メトロ GTFS 184本 平均2.3分 (路線値 55km/h) + (28008, 2800805, 2800806, 59.0), // 東京メトロ半蔵門線 九段下〜神保町: 東京メトロ GTFS 184本 平均1.0分 (路線値 55km/h) - (28008, 2800806, 2800807, 75.0), + (28008, 2800806, 2800807, 73.0), // 東京メトロ半蔵門線 神保町〜大手町: 東京メトロ GTFS 184本 平均1.5分 (路線値 55km/h) (28008, 2800807, 2800808, 105.0), // 東京メトロ半蔵門線 大手町〜三越前: 東京メトロ GTFS 184本 平均1.3分 (路線値 55km/h) - (28008, 2800808, 2800809, 93.0), + (28008, 2800808, 2800809, 88.0), // 東京メトロ半蔵門線 三越前〜水天宮前: 東京メトロ GTFS 184本 平均2.3分 (路線値 55km/h) - (28008, 2800809, 2800810, 42.0), - // 東京メトロ半蔵門線 水天宮前〜清澄白河: 東京メトロ GTFS 184本 平均2.4分 (路線値 55km/h) - (28008, 2800810, 2800811, 43.0), + (28008, 2800809, 2800810, 45.0), // 東京メトロ半蔵門線 清澄白河〜住吉: 東京メトロ GTFS 183本 平均2.4分 (路線値 55km/h) - (28008, 2800811, 2800814, 65.0), + (28008, 2800811, 2800814, 81.0), // 東京メトロ半蔵門線 錦糸町〜押上〈スカイツリー前〉: 東京メトロ GTFS 183本 平均1.9分 (路線値 55km/h) (28008, 2800812, 2800813, 87.0), - // 東京メトロ半蔵門線 錦糸町〜住吉: 東京メトロ GTFS 183本 平均1.4分 (路線値 55km/h) - (28008, 2800812, 2800814, 102.0), + // 東京メトロ半蔵門線 錦糸町〜住吉: 東京メトロ GTFS 183本 平均1.3分 (路線値 55km/h) + (28008, 2800812, 2800814, 98.0), // 東京メトロ南北線 赤羽岩淵〜志茂: 東京メトロ GTFS 140本 平均1.5分 (路線値 65km/h) - (28009, 2800901, 2800902, 109.0), + (28009, 2800901, 2800902, 110.0), // 東京メトロ南北線 志茂〜王子神谷: 東京メトロ GTFS 140本 平均1.9分 (路線値 65km/h) - (28009, 2800902, 2800903, 71.0), + (28009, 2800902, 2800903, 69.0), // 東京メトロ南北線 王子〜西ケ原: 東京メトロ GTFS 140本 平均1.4分 (路線値 65km/h) - (28009, 2800904, 2800905, 102.0), + (28009, 2800904, 2800905, 103.0), // 東京メトロ南北線 西ケ原〜駒込: 東京メトロ GTFS 140本 平均1.4分 (路線値 65km/h) (28009, 2800905, 2800906, 106.0), - // 東京メトロ南北線 駒込〜本駒込: 東京メトロ GTFS 140本 平均2.1分 (路線値 65km/h) - (28009, 2800906, 2800907, 70.0), - // 東京メトロ南北線 本駒込〜東大前: 東京メトロ GTFS 140本 平均1.3分 (路線値 65km/h) - (28009, 2800907, 2800908, 93.0), + // 東京メトロ南北線 駒込〜本駒込: 東京メトロ GTFS 140本 平均2.0分 (路線値 65km/h) + (28009, 2800906, 2800907, 76.0), + // 東京メトロ南北線 本駒込〜東大前: 東京メトロ GTFS 140本 平均1.2分 (路線値 65km/h) + (28009, 2800907, 2800908, 92.0), // 東京メトロ南北線 東大前〜後楽園: 東京メトロ GTFS 140本 平均1.5分 (路線値 65km/h) (28009, 2800908, 2800909, 104.0), - // 東京メトロ南北線 後楽園〜飯田橋: 東京メトロ GTFS 140本 平均1.7分 (路線値 65km/h) - (28009, 2800909, 2800910, 55.0), + // 東京メトロ南北線 後楽園〜飯田橋: 東京メトロ GTFS 140本 平均1.6分 (路線値 65km/h) + (28009, 2800909, 2800910, 74.0), // 東京メトロ南北線 市ケ谷〜四ツ谷: 東京メトロ GTFS 140本 平均1.4分 (路線値 65km/h) (28009, 2800911, 2800912, 102.0), // 東京メトロ南北線 四ツ谷〜永田町: 東京メトロ GTFS 140本 平均1.6分 (路線値 65km/h) (28009, 2800912, 2800913, 107.0), - // 東京メトロ南北線 永田町〜溜池山王: 東京メトロ GTFS 140本 平均1.1分 (路線値 65km/h) - (28009, 2800913, 2800914, 78.0), - // 東京メトロ南北線 溜池山王〜六本木一丁目: 東京メトロ GTFS 140本 平均1.7分 (路線値 65km/h) - (28009, 2800914, 2800915, 50.0), + // 東京メトロ南北線 永田町〜溜池山王: 東京メトロ GTFS 140本 平均1.0分 (路線値 65km/h) + (28009, 2800913, 2800914, 77.0), + // 東京メトロ南北線 溜池山王〜六本木一丁目: 東京メトロ GTFS 140本 平均1.6分 (路線値 65km/h) + (28009, 2800914, 2800915, 53.0), // 東京メトロ南北線 麻布十番〜白金高輪: 東京メトロ GTFS 140本 平均1.7分 (路線値 65km/h) (28009, 2800916, 2800917, 81.0), // 東京メトロ南北線 白金高輪〜白金台: 東京メトロ GTFS 79本 平均1.7分 (路線値 65km/h) - (28009, 2800917, 2800918, 48.0), + (28009, 2800917, 2800918, 50.0), // 東京メトロ南北線 白金台〜目黒: 東京メトロ GTFS 79本 平均1.9分 (路線値 65km/h) - (28009, 2800918, 2800919, 50.0), - // 東京メトロ副都心線 和光市〜地下鉄成増: 東京メトロ GTFS 82本 平均2.6分 (路線値 55km/h) - (28010, 2801001, 2801002, 75.0), + (28009, 2800918, 2800919, 55.0), + // 東京メトロ副都心線 和光市〜地下鉄成増: 東京メトロ GTFS 82本 平均2.6分 (路線値 60km/h) + (28010, 2801001, 2801002, 76.0), // 東京メトロ副都心線 地下鉄成増〜地下鉄赤塚: 東京メトロ GTFS 82本 平均1.8分 (路線値 55km/h) (28010, 2801002, 2801003, 79.0), - // 東京メトロ副都心線 地下鉄赤塚〜平和台: 東京メトロ GTFS 82本 平均2.0分 (路線値 55km/h) - (28010, 2801003, 2801004, 77.0), - // 東京メトロ副都心線 小竹向原〜千川: 東京メトロ GTFS 115本 平均1.7分 (路線値 55km/h) - (28010, 2801006, 2801007, 65.0), - // 東京メトロ副都心線 千川〜要町: 東京メトロ GTFS 115本 平均1.4分 (路線値 55km/h) - (28010, 2801007, 2801008, 104.0), - // 東京メトロ副都心線 要町〜池袋: 東京メトロ GTFS 115本 平均1.3分 (路線値 55km/h) - (28010, 2801008, 2801009, 97.0), - // 東京メトロ副都心線 池袋〜雑司が谷: 東京メトロ GTFS 136本 平均1.9分 (路線値 55km/h) - (28010, 2801009, 2801010, 72.0), - // 東京メトロ副都心線 西早稲田〜東新宿: 東京メトロ GTFS 136本 平均1.4分 (路線値 55km/h) - (28010, 2801011, 2801012, 105.0), - // 東京メトロ副都心線 東新宿〜新宿三丁目: 東京メトロ GTFS 136本 平均1.4分 (路線値 55km/h) - (28010, 2801012, 2801013, 72.0), - // 東京メトロ副都心線 新宿三丁目〜北参道: 東京メトロ GTFS 138本 平均2.0分 (路線値 55km/h) - (28010, 2801013, 2801014, 62.0), + // 東京メトロ副都心線 地下鉄赤塚〜平和台: 東京メトロ GTFS 82本 平均2.0分 (路線値 60km/h) + (28010, 2801003, 2801004, 85.0), + // 東京メトロ副都心線 小竹向原〜千川: 東京メトロ GTFS 115本 平均1.5分 (路線値 60km/h) + (28010, 2801006, 2801007, 108.0), + // 東京メトロ副都心線 千川〜要町: 東京メトロ GTFS 115本 平均1.4分 (路線値 60km/h) + (28010, 2801007, 2801008, 103.0), + // 東京メトロ副都心線 要町〜池袋: 東京メトロ GTFS 115本 平均1.3分 (路線値 60km/h) + (28010, 2801008, 2801009, 96.0), + // 東京メトロ副都心線 池袋〜雑司が谷: 東京メトロ GTFS 136本 平均2.0分 (路線値 60km/h) + (28010, 2801009, 2801010, 90.0), + // 東京メトロ副都心線 西早稲田〜東新宿: 東京メトロ GTFS 136本 平均1.4分 (路線値 60km/h) + (28010, 2801011, 2801012, 103.0), + // 東京メトロ副都心線 東新宿〜新宿三丁目: 東京メトロ GTFS 136本 平均1.5分 (路線値 60km/h) + (28010, 2801012, 2801013, 63.0), + // 東京メトロ副都心線 新宿三丁目〜北参道: 東京メトロ GTFS 138本 平均2.0分 (路線値 60km/h) + (28010, 2801013, 2801014, 65.0), // 東京メトロ副都心線 北参道〜明治神宮前〈原宿〉: 東京メトロ GTFS 138本 平均1.5分 (路線値 55km/h) (28010, 2801014, 2801015, 108.0), - // 東京メトロ副都心線 明治神宮前〈原宿〉〜渋谷: 東京メトロ GTFS 138本 平均1.6分 (路線値 55km/h) - (28010, 2801015, 2801016, 59.0), + // 東京メトロ副都心線 明治神宮前〈原宿〉〜渋谷: 東京メトロ GTFS 138本 平均1.6分 (路線値 60km/h) + (28010, 2801015, 2801016, 64.0), // 函館市電2系統 湯の川〜湯の川温泉: 函館市電 GTFS 53本 平均1.6分 (路線値 20km/h) - (99105, 9910501, 9910502, 23.0), + (99105, 9910501, 9910502, 22.0), // 函館市電2系統 函館アリーナ前〜駒場車庫前: 函館市電 GTFS 53本 平均0.9分 (路線値 20km/h) (99105, 9910503, 9910504, 29.0), // 函館市電2系統 競馬場前〜深堀町: 函館市電 GTFS 53本 平均1.7分 (路線値 20km/h) (99105, 9910505, 9910506, 26.0), // 函館市電2系統 深堀町〜柏木町: 函館市電 GTFS 53本 平均1.4分 (路線値 20km/h) - (99105, 9910506, 9910507, 25.0), - // 函館市電2系統 杉並町〜五稜郭公園前: 函館市電 GTFS 53本 平均1.9分 (路線値 20km/h) - (99105, 9910508, 9910509, 28.0), + (99105, 9910506, 9910507, 22.0), + // 函館市電2系統 柏木町〜杉並町: 函館市電 GTFS 53本 平均1.4分 (路線値 20km/h) + (99105, 9910507, 9910508, 28.0), + // 函館市電2系統 杉並町〜五稜郭公園前: 函館市電 GTFS 53本 平均1.7分 (路線値 20km/h) + (99105, 9910508, 9910509, 29.0), // 函館市電2系統 五稜郭公園前〜中央病院前: 函館市電 GTFS 53本 平均2.3分 (路線値 20km/h) (99105, 9910509, 9910510, 9.0), // 函館市電2系統 中央病院前〜千代台: 函館市電 GTFS 53本 平均1.4分 (路線値 20km/h) - (99105, 9910510, 9910511, 23.0), - // 函館市電2系統 堀川町〜昭和橋: 函館市電 GTFS 53本 平均1.4分 (路線値 20km/h) - (99105, 9910512, 9910513, 23.0), + (99105, 9910510, 9910511, 21.0), // 函館市電2系統 千歳町〜新川町: 函館市電 GTFS 53本 平均0.9分 (路線値 20km/h) (99105, 9910514, 9910515, 24.0), // 函館市電2系統 新川町〜松風町: 函館市電 GTFS 53本 平均1.4分 (路線値 20km/h) - (99105, 9910515, 9910516, 31.0), + (99105, 9910515, 9910516, 27.0), // 函館市電2系統 松風町〜函館駅前: 函館市電 GTFS 53本 平均2.7分 (路線値 20km/h) - (99105, 9910516, 9910517, 13.0), + (99105, 9910516, 9910517, 12.0), // 函館市電2系統 函館駅前〜市役所前: 函館市電 GTFS 53本 平均1.4分 (路線値 20km/h) (99105, 9910517, 9910518, 32.0), + // 函館市電2系統 魚市場通〜十字街: 函館市電 GTFS 53本 平均1.5分 (路線値 20km/h) + (99105, 9910519, 9910520, 17.0), // 函館市電2系統 十字街〜宝来町: 函館市電 GTFS 53本 平均2.0分 (路線値 20km/h) - (99105, 9910520, 9910521, 14.0), + (99105, 9910520, 9910521, 12.0), // 函館市電2系統 宝来町〜青柳町: 函館市電 GTFS 53本 平均1.7分 (路線値 20km/h) - (99105, 9910521, 9910522, 28.0), + (99105, 9910521, 9910522, 30.0), // 函館市電2系統 青柳町〜谷地頭: 函館市電 GTFS 53本 平均2.2分 (路線値 20km/h) - (99105, 9910522, 9910523, 13.0), - // 函館市電5系統 湯の川〜湯の川温泉: 函館市電 GTFS 53本 平均1.6分 (路線値 25km/h) - (99106, 9910601, 9910602, 23.0), + (99105, 9910522, 9910523, 12.0), + // 函館市電5系統 湯の川〜湯の川温泉: 函館市電 GTFS 53本 平均1.6分 (路線値 20km/h) + (99106, 9910601, 9910602, 22.0), // 函館市電5系統 函館アリーナ前〜駒場車庫前: 函館市電 GTFS 53本 平均0.9分 (路線値 25km/h) (99106, 9910603, 9910604, 29.0), - // 函館市電5系統 柏木町〜杉並町: 函館市電 GTFS 53本 平均1.4分 (路線値 25km/h) - (99106, 9910607, 9910608, 32.0), - // 函館市電5系統 杉並町〜五稜郭公園前: 函館市電 GTFS 53本 平均1.7分 (路線値 25km/h) - (99106, 9910608, 9910609, 33.0), - // 函館市電5系統 中央病院前〜千代台: 函館市電 GTFS 53本 平均1.4分 (路線値 25km/h) - (99106, 9910610, 9910611, 23.0), + // 函館市電5系統 深堀町〜柏木町: 函館市電 GTFS 53本 平均1.4分 (路線値 20km/h) + (99106, 9910606, 9910607, 22.0), + // 函館市電5系統 柏木町〜杉並町: 函館市電 GTFS 53本 平均1.4分 (路線値 20km/h) + (99106, 9910607, 9910608, 28.0), + // 函館市電5系統 杉並町〜五稜郭公園前: 函館市電 GTFS 53本 平均1.7分 (路線値 20km/h) + (99106, 9910608, 9910609, 29.0), + // 函館市電5系統 中央病院前〜千代台: 函館市電 GTFS 53本 平均1.4分 (路線値 20km/h) + (99106, 9910610, 9910611, 21.0), // 函館市電5系統 千代台〜堀川町: 函館市電 GTFS 53本 平均1.4分 (路線値 25km/h) (99106, 9910611, 9910612, 40.0), - // 函館市電5系統 堀川町〜昭和橋: 函館市電 GTFS 53本 平均1.4分 (路線値 25km/h) - (99106, 9910612, 9910613, 23.0), // 函館市電5系統 昭和橋〜千歳町: 函館市電 GTFS 53本 平均0.9分 (路線値 25km/h) (99106, 9910613, 9910614, 31.0), - // 函館市電5系統 新川町〜松風町: 函館市電 GTFS 53本 平均1.3分 (路線値 25km/h) - (99106, 9910615, 9910616, 32.0), - // 函館市電5系統 松風町〜函館駅前: 函館市電 GTFS 53本 平均2.7分 (路線値 25km/h) - (99106, 9910616, 9910617, 13.0), + // 函館市電5系統 新川町〜松風町: 函館市電 GTFS 53本 平均1.4分 (路線値 20km/h) + (99106, 9910615, 9910616, 27.0), + // 函館市電5系統 松風町〜函館駅前: 函館市電 GTFS 53本 平均2.7分 (路線値 20km/h) + (99106, 9910616, 9910617, 12.0), // 函館市電5系統 函館駅前〜市役所前: 函館市電 GTFS 53本 平均1.4分 (路線値 25km/h) (99106, 9910617, 9910618, 31.0), - // 函館市電5系統 魚市場通〜十字街: 函館市電 GTFS 53本 平均1.4分 (路線値 25km/h) - (99106, 9910619, 9910620, 20.0), - // 函館市電5系統 十字街〜末広町: 函館市電 GTFS 53本 平均3.0分 (路線値 25km/h) - (99106, 9910620, 9910621, 17.0), + // 函館市電5系統 魚市場通〜十字街: 函館市電 GTFS 53本 平均1.4分 (路線値 20km/h) + (99106, 9910619, 9910620, 18.0), + // 函館市電5系統 十字街〜末広町: 函館市電 GTFS 53本 平均3.0分 (路線値 20km/h) + (99106, 9910620, 9910621, 15.0), // 函館市電5系統 大町〜函館どつく前: 函館市電 GTFS 53本 平均1.4分 (路線値 25km/h) (99106, 9910622, 9910623, 32.0), - // 都営大江戸線 都庁前〜新宿西口: 都営地下鉄 GTFS 145本 平均1.0分 (路線値 70km/h) - (99301, 9930100, 9930102, 87.0), - // 都営大江戸線 都庁前〜新宿: 都営地下鉄 GTFS 146本 平均1.6分 (路線値 70km/h) - (99301, 9930101, 9930128, 32.0), - // 都営大江戸線 都庁前〜西新宿五丁目: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 70km/h) - (99301, 9930101, 9930129, 92.0), - // 都営大江戸線 新宿西口〜東新宿: 都営地下鉄 GTFS 145本 平均2.0分 (路線値 70km/h) - (99301, 9930102, 9930103, 40.0), - // 都営大江戸線 東新宿〜若松河田: 都営地下鉄 GTFS 145本 平均1.5分 (路線値 70km/h) - (99301, 9930103, 9930104, 74.0), - // 都営大江戸線 若松河田〜牛込柳町: 都営地下鉄 GTFS 145本 平均1.0分 (路線値 70km/h) - (99301, 9930104, 9930105, 83.0), - // 都営大江戸線 牛込柳町〜牛込神楽坂: 都営地下鉄 GTFS 145本 平均1.2分 (路線値 70km/h) - (99301, 9930105, 9930106, 105.0), - // 都営大江戸線 牛込神楽坂〜飯田橋: 都営地下鉄 GTFS 145本 平均2.0分 (路線値 70km/h) - (99301, 9930106, 9930107, 37.0), + // 都営大江戸線 都庁前〜新宿西口: 都営地下鉄 GTFS 145本 平均1.0分 (路線値 60km/h) + (99301, 9930100, 9930102, 90.0), + // 都営大江戸線 都庁前〜新宿: 都営地下鉄 GTFS 146本 平均1.6分 (路線値 60km/h) + (99301, 9930101, 9930128, 37.0), + // 都営大江戸線 都庁前〜西新宿五丁目: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 60km/h) + (99301, 9930101, 9930129, 89.0), + // 都営大江戸線 東新宿〜若松河田: 都営地下鉄 GTFS 145本 平均1.5分 (路線値 60km/h) + (99301, 9930103, 9930104, 66.0), + // 都営大江戸線 若松河田〜牛込柳町: 都営地下鉄 GTFS 145本 平均1.0分 (路線値 60km/h) + (99301, 9930104, 9930105, 81.0), + // 都営大江戸線 牛込柳町〜牛込神楽坂: 都営地下鉄 GTFS 145本 平均1.2分 (路線値 60km/h) + (99301, 9930105, 9930106, 102.0), + // 都営大江戸線 牛込神楽坂〜飯田橋: 都営地下鉄 GTFS 145本 平均2.0分 (路線値 60km/h) + (99301, 9930106, 9930107, 38.0), // 都営大江戸線 飯田橋〜春日: 都営地下鉄 GTFS 145本 平均1.4分 (路線値 70km/h) (99301, 9930107, 9930108, 109.0), - // 都営大江戸線 春日〜本郷三丁目: 都営地下鉄 GTFS 145本 平均1.0分 (路線値 70km/h) - (99301, 9930108, 9930109, 86.0), - // 都営大江戸線 本郷三丁目〜上野御徒町: 都営地下鉄 GTFS 145本 平均2.0分 (路線値 70km/h) - (99301, 9930109, 9930110, 62.0), - // 都営大江戸線 上野御徒町〜新御徒町: 都営地下鉄 GTFS 145本 平均1.0分 (路線値 70km/h) - (99301, 9930110, 9930111, 88.0), + // 都営大江戸線 春日〜本郷三丁目: 都営地下鉄 GTFS 145本 平均1.0分 (路線値 60km/h) + (99301, 9930108, 9930109, 87.0), + // 都営大江戸線 本郷三丁目〜上野御徒町: 都営地下鉄 GTFS 145本 平均2.0分 (路線値 60km/h) + (99301, 9930109, 9930110, 57.0), + // 都営大江戸線 上野御徒町〜新御徒町: 都営地下鉄 GTFS 145本 平均1.0分 (路線値 60km/h) + (99301, 9930110, 9930111, 84.0), // 都営大江戸線 新御徒町〜蔵前: 都営地下鉄 GTFS 145本 平均1.0分 (路線値 70km/h) (99301, 9930111, 9930112, 101.0), - // 都営大江戸線 蔵前〜両国: 都営地下鉄 GTFS 145本 平均2.0分 (路線値 70km/h) - (99301, 9930112, 9930113, 40.0), - // 都営大江戸線 両国〜森下: 都営地下鉄 GTFS 145本 平均1.0分 (路線値 70km/h) - (99301, 9930113, 9930114, 106.0), - // 都営大江戸線 森下〜清澄白河: 都営地下鉄 GTFS 145本 平均1.0分 (路線値 70km/h) - (99301, 9930114, 9930115, 87.0), - // 都営大江戸線 清澄白河〜門前仲町: 都営地下鉄 GTFS 146本 平均2.0分 (路線値 70km/h) - (99301, 9930115, 9930116, 54.0), - // 都営大江戸線 月島〜勝どき: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 70km/h) - (99301, 9930117, 9930118, 106.0), - // 都営大江戸線 勝どき〜築地市場: 都営地下鉄 GTFS 146本 平均2.0分 (路線値 70km/h) - (99301, 9930118, 9930119, 51.0), - // 都営大江戸線 築地市場〜汐留: 都営地下鉄 GTFS 146本 平均1.7分 (路線値 70km/h) - (99301, 9930119, 9930120, 30.0), - // 都営大江戸線 汐留〜大門: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 70km/h) - (99301, 9930120, 9930121, 103.0), - // 都営大江戸線 大門〜赤羽橋: 都営地下鉄 GTFS 146本 平均2.0分 (路線値 70km/h) - (99301, 9930121, 9930122, 45.0), - // 都営大江戸線 赤羽橋〜麻布十番: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 70km/h) - (99301, 9930122, 9930123, 89.0), - // 都営大江戸線 六本木〜青山一丁目: 都営地下鉄 GTFS 146本 平均2.0分 (路線値 70km/h) - (99301, 9930124, 9930125, 57.0), - // 都営大江戸線 青山一丁目〜国立競技場: 都営地下鉄 GTFS 146本 平均2.0分 (路線値 70km/h) - (99301, 9930125, 9930126, 51.0), - // 都営大江戸線 国立競技場〜代々木: 都営地下鉄 GTFS 146本 平均2.0分 (路線値 70km/h) - (99301, 9930126, 9930127, 60.0), - // 都営大江戸線 代々木〜新宿: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 70km/h) - (99301, 9930127, 9930128, 86.0), - // 都営大江戸線 西新宿五丁目〜中野坂上: 都営地下鉄 GTFS 146本 平均2.0分 (路線値 70km/h) - (99301, 9930129, 9930130, 36.0), - // 都営大江戸線 中野坂上〜東中野: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 70km/h) - (99301, 9930130, 9930131, 111.0), - // 都営大江戸線 東中野〜中井: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 70km/h) - (99301, 9930131, 9930132, 98.0), - // 都営大江戸線 中井〜落合南長崎: 都営地下鉄 GTFS 146本 平均2.0分 (路線値 70km/h) - (99301, 9930132, 9930133, 50.0), - // 都営大江戸線 落合南長崎〜新江古田: 都営地下鉄 GTFS 146本 平均2.0分 (路線値 70km/h) - (99301, 9930133, 9930134, 78.0), - // 都営大江戸線 新江古田〜練馬: 都営地下鉄 GTFS 146本 平均2.0分 (路線値 70km/h) - (99301, 9930134, 9930135, 80.0), - // 都営大江戸線 練馬〜豊島園: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 70km/h) - (99301, 9930135, 9930136, 93.0), - // 都営大江戸線 豊島園〜練馬春日町: 都営地下鉄 GTFS 146本 平均2.0分 (路線値 70km/h) - (99301, 9930136, 9930137, 60.0), + // 都営大江戸線 両国〜森下: 都営地下鉄 GTFS 145本 平均1.0分 (路線値 60km/h) + (99301, 9930113, 9930114, 105.0), + // 都営大江戸線 森下〜清澄白河: 都営地下鉄 GTFS 145本 平均1.0分 (路線値 60km/h) + (99301, 9930114, 9930115, 88.0), + // 都営大江戸線 清澄白河〜門前仲町: 都営地下鉄 GTFS 146本 平均2.0分 (路線値 60km/h) + (99301, 9930115, 9930116, 48.0), + // 都営大江戸線 月島〜勝どき: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 60km/h) + (99301, 9930117, 9930118, 101.0), + // 都営大江戸線 築地市場〜汐留: 都営地下鉄 GTFS 146本 平均2.0分 (路線値 60km/h) + (99301, 9930119, 9930120, 33.0), + // 都営大江戸線 汐留〜大門: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 60km/h) + (99301, 9930120, 9930121, 105.0), + // 都営大江戸線 大門〜赤羽橋: 都営地下鉄 GTFS 146本 平均2.0分 (路線値 60km/h) + (99301, 9930121, 9930122, 53.0), + // 都営大江戸線 赤羽橋〜麻布十番: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 60km/h) + (99301, 9930122, 9930123, 85.0), + // 都営大江戸線 麻布十番〜六本木: 都営地下鉄 GTFS 146本 平均1.5分 (路線値 60km/h) + (99301, 9930123, 9930124, 75.0), + // 都営大江戸線 六本木〜青山一丁目: 都営地下鉄 GTFS 146本 平均2.0分 (路線値 60km/h) + (99301, 9930124, 9930125, 55.0), + // 都営大江戸線 青山一丁目〜国立競技場: 都営地下鉄 GTFS 146本 平均2.0分 (路線値 60km/h) + (99301, 9930125, 9930126, 52.0), + // 都営大江戸線 国立競技場〜代々木: 都営地下鉄 GTFS 146本 平均2.0分 (路線値 60km/h) + (99301, 9930126, 9930127, 63.0), + // 都営大江戸線 代々木〜新宿: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 60km/h) + (99301, 9930127, 9930128, 83.0), + // 都営大江戸線 西新宿五丁目〜中野坂上: 都営地下鉄 GTFS 146本 平均2.0分 (路線値 60km/h) + (99301, 9930129, 9930130, 40.0), + // 都営大江戸線 中野坂上〜東中野: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 60km/h) + (99301, 9930130, 9930131, 106.0), + // 都営大江戸線 東中野〜中井: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 60km/h) + (99301, 9930131, 9930132, 93.0), + // 都営大江戸線 中井〜落合南長崎: 都営地下鉄 GTFS 146本 平均2.0分 (路線値 60km/h) + (99301, 9930132, 9930133, 55.0), + // 都営大江戸線 落合南長崎〜新江古田: 都営地下鉄 GTFS 146本 平均2.0分 (路線値 60km/h) + (99301, 9930133, 9930134, 72.0), + // 都営大江戸線 新江古田〜練馬: 都営地下鉄 GTFS 146本 平均2.0分 (路線値 60km/h) + (99301, 9930134, 9930135, 71.0), + // 都営大江戸線 練馬〜豊島園: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 60km/h) + (99301, 9930135, 9930136, 101.0), // 都営浅草線 西馬込〜馬込: 都営地下鉄 GTFS 100本 平均1.6分 (路線値 65km/h) (99302, 9930201, 9930202, 75.0), // 都営浅草線 馬込〜中延: 都営地下鉄 GTFS 100本 平均1.0分 (路線値 65km/h) (99302, 9930202, 9930203, 104.0), // 都営浅草線 中延〜戸越: 都営地下鉄 GTFS 100本 平均1.0分 (路線値 65km/h) - (99302, 9930203, 9930204, 103.0), + (99302, 9930203, 9930204, 102.0), // 都営浅草線 戸越〜五反田: 都営地下鉄 GTFS 100本 平均2.0分 (路線値 65km/h) (99302, 9930204, 9930205, 72.0), // 都営浅草線 五反田〜高輪台: 都営地下鉄 GTFS 100本 平均1.0分 (路線値 65km/h) (99302, 9930205, 9930206, 88.0), // 都営浅草線 高輪台〜泉岳寺: 都営地下鉄 GTFS 100本 平均2.2分 (路線値 65km/h) - (99302, 9930206, 9930207, 43.0), + (99302, 9930206, 9930207, 49.0), // 都営浅草線 三田〜大門: 都営地下鉄 GTFS 166本 平均1.8分 (路線値 65km/h) - (99302, 9930208, 9930209, 53.0), + (99302, 9930208, 9930209, 60.0), // 都営浅草線 大門〜新橋: 都営地下鉄 GTFS 166本 平均1.3分 (路線値 65km/h) (99302, 9930209, 9930210, 105.0), // 都営浅草線 新橋〜東銀座: 都営地下鉄 GTFS 166本 平均1.7分 (路線値 65km/h) - (99302, 9930210, 9930211, 40.0), + (99302, 9930210, 9930211, 43.0), // 都営浅草線 東銀座〜宝町: 都営地下鉄 GTFS 166本 平均1.0分 (路線値 65km/h) - (99302, 9930211, 9930212, 90.0), + (99302, 9930211, 9930212, 89.0), // 都営浅草線 宝町〜日本橋: 都営地下鉄 GTFS 166本 平均1.0分 (路線値 65km/h) - (99302, 9930212, 9930213, 90.0), + (99302, 9930212, 9930213, 89.0), // 都営浅草線 日本橋〜人形町: 都営地下鉄 GTFS 166本 平均1.0分 (路線値 65km/h) - (99302, 9930213, 9930214, 90.0), + (99302, 9930213, 9930214, 91.0), // 都営浅草線 人形町〜東日本橋: 都営地下鉄 GTFS 166本 平均1.0分 (路線値 65km/h) - (99302, 9930214, 9930215, 84.0), + (99302, 9930214, 9930215, 88.0), // 都営浅草線 東日本橋〜浅草橋: 都営地下鉄 GTFS 166本 平均1.0分 (路線値 65km/h) (99302, 9930215, 9930216, 79.0), // 都営浅草線 浅草橋〜蔵前: 都営地下鉄 GTFS 166本 平均1.0分 (路線値 65km/h) (99302, 9930216, 9930217, 89.0), // 都営浅草線 蔵前〜浅草: 都営地下鉄 GTFS 166本 平均1.0分 (路線値 65km/h) - (99302, 9930217, 9930218, 96.0), + (99302, 9930217, 9930218, 95.0), // 都営浅草線 浅草〜本所吾妻橋: 都営地下鉄 GTFS 166本 平均1.0分 (路線値 65km/h) - (99302, 9930218, 9930219, 85.0), + (99302, 9930218, 9930219, 86.0), // 都営浅草線 本所吾妻橋〜押上(スカイツリー前): 都営地下鉄 GTFS 166本 平均1.0分 (路線値 65km/h) - (99302, 9930219, 9930220, 90.0), + (99302, 9930219, 9930220, 91.0), // 都営三田線 目黒〜白金台: 都営地下鉄 GTFS 90本 平均2.0分 (路線値 75km/h) - (99303, 9930301, 9930302, 46.0), + (99303, 9930301, 9930302, 50.0), // 都営三田線 白金台〜白金高輪: 都営地下鉄 GTFS 90本 平均1.8分 (路線値 75km/h) - (99303, 9930302, 9930303, 43.0), + (99303, 9930302, 9930303, 45.0), // 都営三田線 白金高輪〜三田: 都営地下鉄 GTFS 146本 平均2.8分 (路線値 75km/h) - (99303, 9930303, 9930304, 40.0), + (99303, 9930303, 9930304, 46.0), // 都営三田線 三田〜芝公園: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) - (99303, 9930304, 9930305, 83.0), + (99303, 9930304, 9930305, 82.0), // 都営三田線 芝公園〜御成門: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) - (99303, 9930305, 9930306, 92.0), + (99303, 9930305, 9930306, 91.0), // 都営三田線 御成門〜内幸町: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) - (99303, 9930306, 9930307, 103.0), + (99303, 9930306, 9930307, 102.0), // 都営三田線 内幸町〜日比谷: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) - (99303, 9930307, 9930308, 92.0), + (99303, 9930307, 9930308, 91.0), // 都営三田線 日比谷〜大手町: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) - (99303, 9930308, 9930309, 103.0), + (99303, 9930308, 9930309, 102.0), // 都営三田線 大手町〜神保町: 都営地下鉄 GTFS 146本 平均2.2分 (路線値 75km/h) - (99303, 9930309, 9930310, 48.0), + (99303, 9930309, 9930310, 55.0), // 都営三田線 神保町〜水道橋: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) (99303, 9930310, 9930311, 98.0), // 都営三田線 水道橋〜春日: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) - (99303, 9930311, 9930312, 86.0), + (99303, 9930311, 9930312, 85.0), // 都営三田線 春日〜白山: 都営地下鉄 GTFS 146本 平均1.6分 (路線値 75km/h) - (99303, 9930312, 9930313, 117.0), + (99303, 9930312, 9930313, 106.0), // 都営三田線 白山〜千石: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) - (99303, 9930313, 9930314, 102.0), + (99303, 9930313, 9930314, 106.0), // 都営三田線 千石〜巣鴨: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) - (99303, 9930314, 9930315, 94.0), + (99303, 9930314, 9930315, 92.0), // 都営三田線 西巣鴨〜新板橋: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) (99303, 9930316, 9930317, 101.0), // 都営三田線 新板橋〜板橋区役所前: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) - (99303, 9930317, 9930318, 99.0), + (99303, 9930317, 9930318, 100.0), // 都営三田線 板橋区役所前〜板橋本町: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) - (99303, 9930318, 9930319, 112.0), + (99303, 9930318, 9930319, 111.0), // 都営三田線 板橋本町〜本蓮沼: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) - (99303, 9930319, 9930320, 96.0), + (99303, 9930319, 9930320, 95.0), // 都営三田線 本蓮沼〜志村坂上: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) - (99303, 9930320, 9930321, 102.0), + (99303, 9930320, 9930321, 101.0), // 都営三田線 志村坂上〜志村三丁目: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) - (99303, 9930321, 9930322, 96.0), + (99303, 9930321, 9930322, 101.0), // 都営三田線 志村三丁目〜蓮根: 都営地下鉄 GTFS 146本 平均1.3分 (路線値 75km/h) - (99303, 9930322, 9930323, 102.0), + (99303, 9930322, 9930323, 110.0), + // 都営三田線 蓮根〜西台: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) + (99303, 9930323, 9930324, 82.0), // 都営三田線 西台〜高島平: 都営地下鉄 GTFS 146本 平均1.1分 (路線値 75km/h) - (99303, 9930324, 9930325, 111.0), + (99303, 9930324, 9930325, 110.0), // 都営三田線 高島平〜新高島平: 都営地下鉄 GTFS 140本 平均1.0分 (路線値 75km/h) - (99303, 9930325, 9930326, 82.0), + (99303, 9930325, 9930326, 81.0), // 都営三田線 新高島平〜西高島平: 都営地下鉄 GTFS 140本 平均1.0分 (路線値 75km/h) - (99303, 9930326, 9930327, 90.0), - // 都営新宿線 新宿〜新宿三丁目: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) - (99304, 9930401, 9930402, 85.0), - // 都営新宿線 新宿三丁目〜曙橋: 都営地下鉄 GTFS 146本 平均2.0分 (路線値 75km/h) - (99304, 9930402, 9930403, 68.0), - // 都営新宿線 曙橋〜市ヶ谷: 都営地下鉄 GTFS 146本 平均2.0分 (路線値 75km/h) - (99304, 9930403, 9930404, 57.0), - // 都営新宿線 市ヶ谷〜九段下: 都営地下鉄 GTFS 146本 平均1.1分 (路線値 75km/h) - (99304, 9930404, 9930405, 118.0), - // 都営新宿線 九段下〜神保町: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) - (99304, 9930405, 9930406, 59.0), - // 都営新宿線 神保町〜小川町: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) - (99304, 9930406, 9930407, 92.0), - // 都営新宿線 小川町〜岩本町: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) - (99304, 9930407, 9930408, 92.0), - // 都営新宿線 岩本町〜馬喰横山: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) - (99304, 9930408, 9930409, 86.0), - // 都営新宿線 馬喰横山〜浜町: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) - (99304, 9930409, 9930410, 80.0), - // 都営新宿線 浜町〜森下: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) + (99303, 9930326, 9930327, 89.0), + // 都営新宿線 新宿三丁目〜曙橋: 都営地下鉄 GTFS 146本 平均2.0分 (路線値 85km/h) + (99304, 9930402, 9930403, 70.0), + // 都営新宿線 曙橋〜市ヶ谷: 都営地下鉄 GTFS 146本 平均2.0分 (路線値 85km/h) + (99304, 9930403, 9930404, 60.0), + // 都営新宿線 市ヶ谷〜九段下: 都営地下鉄 GTFS 146本 平均1.1分 (路線値 85km/h) + (99304, 9930404, 9930405, 119.0), + // 都営新宿線 九段下〜神保町: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 85km/h) + (99304, 9930405, 9930406, 61.0), + // 都営新宿線 神保町〜小川町: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 85km/h) + (99304, 9930406, 9930407, 96.0), + // 都営新宿線 小川町〜岩本町: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 85km/h) + (99304, 9930407, 9930408, 93.0), + // 都営新宿線 浜町〜森下: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 85km/h) (99304, 9930410, 9930411, 91.0), - // 都営新宿線 森下〜菊川: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) + // 都営新宿線 森下〜菊川: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 85km/h) (99304, 9930411, 9930412, 91.0), - // 都営新宿線 菊川〜住吉: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) + // 都営新宿線 菊川〜住吉: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 85km/h) (99304, 9930412, 9930413, 95.0), - // 都営新宿線 住吉〜西大島: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) + // 都営新宿線 住吉〜西大島: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 85km/h) (99304, 9930413, 9930414, 99.0), - // 都営新宿線 西大島〜大島: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 75km/h) + // 都営新宿線 西大島〜大島: 都営地下鉄 GTFS 146本 平均1.0分 (路線値 85km/h) (99304, 9930414, 9930415, 94.0), - // 都営新宿線 大島〜東大島: 都営地下鉄 GTFS 142本 平均2.0分 (路線値 75km/h) + // 都営新宿線 大島〜東大島: 都営地下鉄 GTFS 142本 平均2.0分 (路線値 85km/h) (99304, 9930415, 9930416, 37.0), - // 都営新宿線 東大島〜船堀: 都営地下鉄 GTFS 142本 平均2.0分 (路線値 75km/h) - (99304, 9930416, 9930417, 90.0), - // 都営新宿線 船堀〜一之江: 都営地下鉄 GTFS 142本 平均2.0分 (路線値 75km/h) - (99304, 9930417, 9930418, 82.0), - // 都営新宿線 一之江〜瑞江: 都営地下鉄 GTFS 142本 平均2.0分 (路線値 75km/h) - (99304, 9930418, 9930419, 71.0), - // 都営新宿線 瑞江〜篠崎: 都営地下鉄 GTFS 142本 平均2.0分 (路線値 75km/h) - (99304, 9930419, 9930420, 68.0), + // 都営新宿線 東大島〜船堀: 都営地下鉄 GTFS 142本 平均2.0分 (路線値 85km/h) + (99304, 9930416, 9930417, 91.0), + // 都営新宿線 一之江〜瑞江: 都営地下鉄 GTFS 142本 平均2.0分 (路線値 85km/h) + (99304, 9930418, 9930419, 78.0), + // 都営新宿線 瑞江〜篠崎: 都営地下鉄 GTFS 142本 平均2.0分 (路線値 85km/h) + (99304, 9930419, 9930420, 69.0), + // 都営新宿線 篠崎〜本八幡: 都営地下鉄 GTFS 142本 平均3.0分 (路線値 85km/h) + (99304, 9930420, 9930421, 80.0), // 東京さくらトラム(都電荒川線) 三ノ輪橋〜荒川一中前: 都営地下鉄 GTFS 138本 平均1.2分 (路線値 25km/h) (99305, 9930501, 9930502, 18.0), // 東京さくらトラム(都電荒川線) 町屋駅前〜町屋二丁目: 都営地下鉄 GTFS 99本 平均2.4分 (路線値 25km/h) - (99305, 9930506, 9930507, 14.0), - // 東京さくらトラム(都電荒川線) 宮ノ前〜小台: 都営地下鉄 GTFS 102本 平均1.0分 (路線値 25km/h) - (99305, 9930510, 9930511, 27.0), + (99305, 9930506, 9930507, 13.0), // 東京さくらトラム(都電荒川線) 小台〜荒川遊園地前: 都営地下鉄 GTFS 98本 平均1.0分 (路線値 25km/h) - (99305, 9930511, 9930512, 36.0), + (99305, 9930511, 9930512, 32.0), // 東京さくらトラム(都電荒川線) 荒川遊園地前〜荒川車庫前: 都営地下鉄 GTFS 105本 平均3.5分 (路線値 25km/h) (99305, 9930512, 9930513, 10.0), // 東京さくらトラム(都電荒川線) 荒川車庫前〜梶原: 都営地下鉄 GTFS 99本 平均2.9分 (路線値 25km/h) - (99305, 9930513, 9930514, 13.0), + (99305, 9930513, 9930514, 11.0), // 東京さくらトラム(都電荒川線) 栄町〜王子駅前: 都営地下鉄 GTFS 102本 平均3.0分 (路線値 25km/h) (99305, 9930515, 9930516, 10.0), // 東京さくらトラム(都電荒川線) 滝野川一丁目〜西ヶ原四丁目: 都営地下鉄 GTFS 103本 平均2.0分 (路線値 25km/h) - (99305, 9930518, 9930519, 16.0), + (99305, 9930518, 9930519, 15.0), // 東京さくらトラム(都電荒川線) 西ヶ原四丁目〜新庚申塚: 都営地下鉄 GTFS 97本 平均2.0分 (路線値 25km/h) - (99305, 9930519, 9930520, 16.0), + (99305, 9930519, 9930520, 15.0), // 東京さくらトラム(都電荒川線) 巣鴨新田〜大塚駅前: 都営地下鉄 GTFS 101本 平均3.0分 (路線値 25km/h) (99305, 9930522, 9930523, 12.0), // 東京さくらトラム(都電荒川線) 大塚駅前〜向原: 都営地下鉄 GTFS 97本 平均3.1分 (路線値 25km/h) (99305, 9930523, 9930524, 12.0), - // 東京さくらトラム(都電荒川線) 向原〜東池袋四丁目: 都営地下鉄 GTFS 139本 平均1.6分 (路線値 25km/h) - (99305, 9930524, 9930525, 33.0), + // 東京さくらトラム(都電荒川線) 向原〜東池袋四丁目: 都営地下鉄 GTFS 139本 平均1.4分 (路線値 25km/h) + (99305, 9930524, 9930525, 38.0), // 東京さくらトラム(都電荒川線) 東池袋四丁目〜都電雑司ヶ谷: 都営地下鉄 GTFS 97本 平均1.0分 (路線値 25km/h) - (99305, 9930525, 9930526, 18.0), + (99305, 9930525, 9930526, 17.0), // 東京さくらトラム(都電荒川線) 学習院下〜面影橋: 都営地下鉄 GTFS 139本 平均1.2分 (路線値 25km/h) (99305, 9930528, 9930529, 32.0), // 東京さくらトラム(都電荒川線) 面影橋〜早稲田: 都営地下鉄 GTFS 139本 平均1.2分 (路線値 25km/h) (99305, 9930529, 9930530, 31.0), // つくばエクスプレス線 秋葉原〜新御徒町: つくばエクスプレス GTFS 86本 平均2.1分 (路線値 95km/h) - (99309, 9930901, 9930902, 47.0), + (99309, 9930901, 9930902, 64.0), // つくばエクスプレス線 新御徒町〜浅草: つくばエクスプレス GTFS 86本 平均1.9分 (路線値 95km/h) - (99309, 9930902, 9930903, 55.0), - // つくばエクスプレス線 浅草〜南千住: つくばエクスプレス GTFS 86本 平均2.4分 (路線値 95km/h) - (99309, 9930903, 9930904, 84.0), - // つくばエクスプレス線 南千住〜北千住: つくばエクスプレス GTFS 86本 平均2.0分 (路線値 95km/h) - (99309, 9930904, 9930905, 145.0), + (99309, 9930902, 9930903, 76.0), + // つくばエクスプレス線 南千住〜北千住: つくばエクスプレス GTFS 86本 平均1.9分 (路線値 95km/h) + (99309, 9930904, 9930905, 142.0), // つくばエクスプレス線 北千住〜青井: つくばエクスプレス GTFS 86本 平均2.4分 (路線値 95km/h) - (99309, 9930905, 9930906, 141.0), + (99309, 9930905, 9930906, 135.0), // つくばエクスプレス線 青井〜六町: つくばエクスプレス GTFS 86本 平均1.7分 (路線値 95km/h) - (99309, 9930906, 9930907, 125.0), + (99309, 9930906, 9930907, 123.0), // つくばエクスプレス線 六町〜八潮: つくばエクスプレス GTFS 86本 平均3.0分 (路線値 95km/h) - (99309, 9930907, 9930908, 101.0), - // つくばエクスプレス線 八潮〜三郷中央: つくばエクスプレス GTFS 86本 平均2.8分 (路線値 95km/h) - (99309, 9930908, 9930909, 127.0), + (99309, 9930907, 9930908, 110.0), + // つくばエクスプレス線 八潮〜三郷中央: つくばエクスプレス GTFS 86本 平均2.7分 (路線値 95km/h) + (99309, 9930908, 9930909, 149.0), // つくばエクスプレス線 三郷中央〜南流山: つくばエクスプレス GTFS 86本 平均2.4分 (路線値 95km/h) - (99309, 9930909, 9930910, 129.0), + (99309, 9930909, 9930910, 133.0), // つくばエクスプレス線 南流山〜流山セントラルパーク: つくばエクスプレス GTFS 86本 平均2.0分 (路線値 95km/h) (99309, 9930910, 9930911, 148.0), // つくばエクスプレス線 流山セントラルパーク〜流山おおたかの森: つくばエクスプレス GTFS 86本 平均2.0分 (路線値 95km/h) - (99309, 9930911, 9930912, 150.0), - // つくばエクスプレス線 柏の葉キャンパス〜柏たなか: つくばエクスプレス GTFS 86本 平均2.0分 (路線値 95km/h) - (99309, 9930913, 9930914, 147.0), + (99309, 9930911, 9930912, 149.0), + // つくばエクスプレス線 柏の葉キャンパス〜柏たなか: つくばエクスプレス GTFS 86本 平均1.9分 (路線値 95km/h) + (99309, 9930913, 9930914, 144.0), // つくばエクスプレス線 柏たなか〜守谷: つくばエクスプレス GTFS 86本 平均4.0分 (路線値 95km/h) - (99309, 9930914, 9930915, 122.0), + (99309, 9930914, 9930915, 127.0), // つくばエクスプレス線 守谷〜みらい平: つくばエクスプレス GTFS 28本 平均4.9分 (路線値 95km/h) - (99309, 9930915, 9930916, 111.0), + (99309, 9930915, 9930916, 109.0), // つくばエクスプレス線 万博記念公園〜研究学園: つくばエクスプレス GTFS 28本 平均2.8分 (路線値 95km/h) (99309, 9930918, 9930919, 127.0), // 横浜市営地下鉄ブルーライン 中川〜センター北: 横浜市営地下鉄 GTFS 69本 平均1.9分 (路線値 75km/h) - (99316, 9931602, 9931603, 62.0), + (99316, 9931602, 9931603, 69.0), // 横浜市営地下鉄ブルーライン センター北〜センター南: 横浜市営地下鉄 GTFS 69本 平均1.0分 (路線値 75km/h) - (99316, 9931603, 9931604, 102.0), + (99316, 9931603, 9931604, 100.0), // 横浜市営地下鉄ブルーライン センター南〜仲町台: 横浜市営地下鉄 GTFS 69本 平均2.1分 (路線値 75km/h) - (99316, 9931604, 9931605, 81.0), + (99316, 9931604, 9931605, 118.0), // 横浜市営地下鉄ブルーライン 仲町台〜新羽: 横浜市営地下鉄 GTFS 69本 平均2.4分 (路線値 75km/h) - (99316, 9931605, 9931606, 84.0), + (99316, 9931605, 9931606, 85.0), // 横浜市営地下鉄ブルーライン 新羽〜北新横浜: 横浜市営地下鉄 GTFS 94本 平均1.5分 (路線値 75km/h) - (99316, 9931606, 9931607, 64.0), + (99316, 9931606, 9931607, 60.0), // 横浜市営地下鉄ブルーライン 北新横浜〜新横浜: 横浜市営地下鉄 GTFS 94本 平均1.2分 (路線値 75km/h) (99316, 9931607, 9931608, 116.0), // 横浜市営地下鉄ブルーライン 新横浜〜岸根公園: 横浜市営地下鉄 GTFS 94本 平均2.0分 (路線値 75km/h) - (99316, 9931608, 9931609, 64.0), + (99316, 9931608, 9931609, 69.0), // 横浜市営地下鉄ブルーライン 岸根公園〜片倉町: 横浜市営地下鉄 GTFS 94本 平均1.0分 (路線値 75km/h) - (99316, 9931609, 9931610, 111.0), + (99316, 9931609, 9931610, 112.0), + // 横浜市営地下鉄ブルーライン 片倉町〜三ツ沢上町: 横浜市営地下鉄 GTFS 94本 平均2.0分 (路線値 75km/h) + (99316, 9931610, 9931611, 103.0), // 横浜市営地下鉄ブルーライン 三ツ沢上町〜三ツ沢下町: 横浜市営地下鉄 GTFS 94本 平均1.0分 (路線値 75km/h) - (99316, 9931611, 9931612, 95.0), + (99316, 9931611, 9931612, 93.0), // 横浜市営地下鉄ブルーライン 三ツ沢下町〜横浜: 横浜市営地下鉄 GTFS 94本 平均2.0分 (路線値 75km/h) - (99316, 9931612, 9931613, 57.0), + (99316, 9931612, 9931613, 63.0), // 横浜市営地下鉄ブルーライン 横浜〜高島町: 横浜市営地下鉄 GTFS 94本 平均1.0分 (路線値 75km/h) - (99316, 9931613, 9931614, 95.0), + (99316, 9931613, 9931614, 93.0), // 横浜市営地下鉄ブルーライン 高島町〜桜木町: 横浜市営地下鉄 GTFS 94本 平均1.0分 (路線値 75km/h) - (99316, 9931614, 9931615, 113.0), + (99316, 9931614, 9931615, 111.0), // 横浜市営地下鉄ブルーライン 桜木町〜関内: 横浜市営地下鉄 GTFS 94本 平均1.0分 (路線値 75km/h) - (99316, 9931615, 9931616, 82.0), + (99316, 9931615, 9931616, 81.0), + // 横浜市営地下鉄ブルーライン 関内〜伊勢佐木長者町: 横浜市営地下鉄 GTFS 94本 平均1.8分 (路線値 75km/h) + (99316, 9931616, 9931617, 32.0), // 横浜市営地下鉄ブルーライン 伊勢佐木長者町〜阪東橋: 横浜市営地下鉄 GTFS 94本 平均1.0分 (路線値 75km/h) - (99316, 9931617, 9931618, 95.0), + (99316, 9931617, 9931618, 93.0), // 横浜市営地下鉄ブルーライン 阪東橋〜吉野町: 横浜市営地下鉄 GTFS 94本 平均1.0分 (路線値 75km/h) - (99316, 9931618, 9931619, 81.0), + (99316, 9931618, 9931619, 79.0), // 横浜市営地下鉄ブルーライン 吉野町〜蒔田: 横浜市営地下鉄 GTFS 94本 平均1.0分 (路線値 75km/h) - (99316, 9931619, 9931620, 102.0), + (99316, 9931619, 9931620, 101.0), // 横浜市営地下鉄ブルーライン 蒔田〜弘明寺: 横浜市営地下鉄 GTFS 94本 平均1.0分 (路線値 75km/h) - (99316, 9931620, 9931621, 105.0), + (99316, 9931620, 9931621, 104.0), // 横浜市営地下鉄ブルーライン 弘明寺〜上大岡: 横浜市営地下鉄 GTFS 94本 平均2.0分 (路線値 75km/h) - (99316, 9931621, 9931622, 87.0), + (99316, 9931621, 9931622, 83.0), // 横浜市営地下鉄ブルーライン 上大岡〜港南中央: 横浜市営地下鉄 GTFS 94本 平均1.0分 (路線値 75km/h) - (99316, 9931622, 9931623, 101.0), + (99316, 9931622, 9931623, 102.0), // 横浜市営地下鉄ブルーライン 港南中央〜上永谷: 横浜市営地下鉄 GTFS 94本 平均2.4分 (路線値 75km/h) - (99316, 9931623, 9931624, 60.0), + (99316, 9931623, 9931624, 62.0), // 横浜市営地下鉄ブルーライン 上永谷〜下永谷: 横浜市営地下鉄 GTFS 94本 平均1.9分 (路線値 75km/h) (99316, 9931624, 9931625, 59.0), // 横浜市営地下鉄ブルーライン 下永谷〜舞岡: 横浜市営地下鉄 GTFS 94本 平均1.0分 (路線値 75km/h) - (99316, 9931625, 9931626, 87.0), + (99316, 9931625, 9931626, 86.0), + // 横浜市営地下鉄ブルーライン 舞岡〜戸塚: 横浜市営地下鉄 GTFS 94本 平均2.0分 (路線値 75km/h) + (99316, 9931626, 9931627, 80.0), // 横浜市営地下鉄ブルーライン 戸塚〜踊場: 横浜市営地下鉄 GTFS 94本 平均1.8分 (路線値 75km/h) (99316, 9931627, 9931628, 105.0), // 横浜市営地下鉄ブルーライン 踊場〜中田: 横浜市営地下鉄 GTFS 71本 平均1.0分 (路線値 75km/h) - (99316, 9931628, 9931629, 95.0), + (99316, 9931628, 9931629, 93.0), // 横浜市営地下鉄ブルーライン 中田〜立場: 横浜市営地下鉄 GTFS 71本 平均1.0分 (路線値 75km/h) (99316, 9931629, 9931630, 106.0), // 横浜市営地下鉄ブルーライン 立場〜下飯田: 横浜市営地下鉄 GTFS 71本 平均2.0分 (路線値 75km/h) - (99316, 9931630, 9931631, 114.0), + (99316, 9931630, 9931631, 115.0), // 横浜市営地下鉄ブルーライン 下飯田〜湘南台: 横浜市営地下鉄 GTFS 71本 平均2.0分 (路線値 75km/h) - (99316, 9931631, 9931632, 81.0), + (99316, 9931631, 9931632, 83.0), // 多摩モノレール 多摩センター〜松が谷: 多摩都市モノレール GTFS 88本 平均1.8分 (路線値 50km/h) - (99334, 9933401, 9933402, 41.0), + (99334, 9933401, 9933402, 42.0), // 多摩モノレール 大塚・帝京大学〜中央大学・明星大学: 多摩都市モノレール GTFS 88本 平均1.4分 (路線値 50km/h) (99334, 9933403, 9933404, 72.0), // 多摩モノレール 中央大学・明星大学〜多摩動物公園: 多摩都市モノレール GTFS 88本 平均2.0分 (路線値 50km/h) - (99334, 9933404, 9933405, 34.0), + (99334, 9933404, 9933405, 41.0), // 多摩モノレール 多摩動物公園〜程久保: 多摩都市モノレール GTFS 88本 平均1.4分 (路線値 50km/h) (99334, 9933405, 9933406, 75.0), // 多摩モノレール 程久保〜高幡不動: 多摩都市モノレール GTFS 88本 平均1.4分 (路線値 50km/h) (99334, 9933406, 9933407, 56.0), // 多摩モノレール 高幡不動〜万願寺: 多摩都市モノレール GTFS 88本 平均2.2分 (路線値 50km/h) - (99334, 9933407, 9933408, 42.0), + (99334, 9933407, 9933408, 44.0), + // 多摩モノレール 甲州街道〜柴崎体育館: 多摩都市モノレール GTFS 88本 平均2.1分 (路線値 50km/h) + (99334, 9933409, 9933410, 61.0), // 多摩モノレール 柴崎体育館〜立川南: 多摩都市モノレール GTFS 88本 平均1.3分 (路線値 50km/h) - (99334, 9933410, 9933411, 62.0), + (99334, 9933410, 9933411, 60.0), // 多摩モノレール 立川南〜立川北: 多摩都市モノレール GTFS 88本 平均0.9分 (路線値 50km/h) - (99334, 9933411, 9933412, 63.0), - // 多摩モノレール 立川北〜高松: 多摩都市モノレール GTFS 88本 平均1.7分 (路線値 50km/h) + (99334, 9933411, 9933412, 64.0), + // 多摩モノレール 立川北〜高松: 多摩都市モノレール GTFS 88本 平均1.6分 (路線値 50km/h) (99334, 9933412, 9933413, 70.0), // 多摩モノレール 高松〜立飛: 多摩都市モノレール GTFS 87本 平均1.1分 (路線値 50km/h) - (99334, 9933413, 9933414, 61.0), - // 多摩モノレール 立飛〜泉体育館: 多摩都市モノレール GTFS 87本 平均1.1分 (路線値 50km/h) - (99334, 9933414, 9933415, 45.0), + (99334, 9933413, 9933414, 59.0), // 多摩モノレール 泉体育館〜砂川七番: 多摩都市モノレール GTFS 87本 平均1.0分 (路線値 50km/h) (99334, 9933415, 9933416, 74.0), // 多摩モノレール 桜街道〜上北台: 多摩都市モノレール GTFS 87本 平均1.5分 (路線値 50km/h) @@ -709,14 +709,14 @@ const SEGMENT_SPEED_OVERRIDES: &[(i32, i32, i32, f64)] = &[ (99337, 9933701, 9933702, 96.0), // りんかい線 国際展示場〜東京テレポート: りんかい線 GTFS 106本 平均1.9分 (路線値 65km/h) (99337, 9933703, 9933704, 72.0), - // りんかい線 東京テレポート〜天王洲アイル: りんかい線 GTFS 104本 平均2.9分 (路線値 65km/h) - (99337, 9933704, 9933705, 79.0), + // りんかい線 東京テレポート〜天王洲アイル: りんかい線 GTFS 104本 平均3.0分 (路線値 65km/h) + (99337, 9933704, 9933705, 80.0), // りんかい線 品川シーサイド〜大井町: りんかい線 GTFS 104本 平均2.0分 (路線値 65km/h) (99337, 9933706, 9933707, 61.0), // りんかい線 大井町〜大崎: りんかい線 GTFS 104本 平均2.7分 (路線値 65km/h) - (99337, 9933707, 9933708, 44.0), + (99337, 9933707, 9933708, 52.0), // 日暮里・舎人ライナー 日暮里〜西日暮里: 都営地下鉄 GTFS 151本 平均1.0分 (路線値 60km/h) - (99342, 9934201, 9934202, 78.0), + (99342, 9934201, 9934202, 81.0), // 日暮里・舎人ライナー 赤土小学校前〜熊野前: 都営地下鉄 GTFS 151本 平均1.0分 (路線値 60km/h) (99342, 9934203, 9934204, 86.0), // 日暮里・舎人ライナー 熊野前〜足立小台: 都営地下鉄 GTFS 151本 平均1.0分 (路線値 60km/h) @@ -732,67 +732,67 @@ const SEGMENT_SPEED_OVERRIDES: &[(i32, i32, i32, f64)] = &[ // 日暮里・舎人ライナー 谷在家〜舎人公園: 都営地下鉄 GTFS 151本 平均1.1分 (路線値 60km/h) (99342, 9934210, 9934211, 92.0), // 横浜市営地下鉄グリーンライン 川和町〜都筑ふれあいの丘: 横浜市営地下鉄 GTFS 85本 平均2.0分 (路線値 75km/h) - (99343, 9934302, 9934303, 67.0), + (99343, 9934302, 9934303, 68.0), // 横浜市営地下鉄グリーンライン 都筑ふれあいの丘〜センター南: 横浜市営地下鉄 GTFS 85本 平均2.0分 (路線値 75km/h) - (99343, 9934303, 9934304, 71.0), + (99343, 9934303, 9934304, 79.0), // 横浜市営地下鉄グリーンライン センター南〜センター北: 横浜市営地下鉄 GTFS 85本 平均1.0分 (路線値 75km/h) - (99343, 9934304, 9934305, 98.0), - // 横浜市営地下鉄グリーンライン センター北〜北山田: 横浜市営地下鉄 GTFS 85本 平均2.0分 (路線値 75km/h) - (99343, 9934305, 9934306, 68.0), + (99343, 9934304, 9934305, 97.0), // 横浜市営地下鉄グリーンライン 北山田〜東山田: 横浜市営地下鉄 GTFS 85本 平均1.6分 (路線値 75km/h) - (99343, 9934306, 9934307, 99.0), + (99343, 9934306, 9934307, 119.0), // 横浜市営地下鉄グリーンライン 東山田〜高田: 横浜市営地下鉄 GTFS 85本 平均2.0分 (路線値 75km/h) - (99343, 9934307, 9934308, 69.0), + (99343, 9934307, 9934308, 70.0), // 横浜市営地下鉄グリーンライン 高田〜日吉本町: 横浜市営地下鉄 GTFS 85本 平均1.0分 (路線値 75km/h) - (99343, 9934308, 9934309, 112.0), + (99343, 9934308, 9934309, 115.0), // 横浜市営地下鉄グリーンライン 日吉本町〜日吉: 横浜市営地下鉄 GTFS 85本 平均1.9分 (路線値 75km/h) - (99343, 9934309, 9934310, 56.0), + (99343, 9934309, 9934310, 62.0), // 京都市営地下鉄烏丸線 国際会館〜松ヶ崎: 京都市営地下鉄 GTFS 127本 平均2.2分 (路線値 75km/h) - (99610, 9961001, 9961002, 58.0), + (99610, 9961001, 9961002, 64.0), // 京都市営地下鉄烏丸線 松ヶ崎〜北山: 京都市営地下鉄 GTFS 127本 平均1.4分 (路線値 75km/h) - (99610, 9961002, 9961003, 101.0), - // 京都市営地下鉄烏丸線 北山〜北大路: 京都市営地下鉄 GTFS 127本 平均1.4分 (路線値 75km/h) - (99610, 9961003, 9961004, 102.0), + (99610, 9961002, 9961003, 100.0), + // 京都市営地下鉄烏丸線 北山〜北大路: 京都市営地下鉄 GTFS 127本 平均1.5分 (路線値 75km/h) + (99610, 9961003, 9961004, 113.0), // 京都市営地下鉄烏丸線 北大路〜鞍馬口: 京都市営地下鉄 GTFS 127本 平均1.3分 (路線値 75km/h) - (99610, 9961004, 9961005, 94.0), + (99610, 9961004, 9961005, 93.0), // 京都市営地下鉄烏丸線 鞍馬口〜今出川: 京都市営地下鉄 GTFS 127本 平均1.2分 (路線値 75km/h) - (99610, 9961005, 9961006, 92.0), + (99610, 9961005, 9961006, 91.0), // 京都市営地下鉄烏丸線 今出川〜丸太町: 京都市営地下鉄 GTFS 127本 平均1.9分 (路線値 75km/h) (99610, 9961006, 9961007, 79.0), // 京都市営地下鉄烏丸線 丸太町〜烏丸御池: 京都市営地下鉄 GTFS 127本 平均1.1分 (路線値 75km/h) - (99610, 9961007, 9961008, 84.0), + (99610, 9961007, 9961008, 83.0), // 京都市営地下鉄烏丸線 烏丸御池〜四条: 京都市営地下鉄 GTFS 127本 平均1.3分 (路線値 75km/h) - (99610, 9961008, 9961009, 97.0), + (99610, 9961008, 9961009, 96.0), // 京都市営地下鉄烏丸線 四条〜五条: 京都市営地下鉄 GTFS 127本 平均1.2分 (路線値 75km/h) - (99610, 9961009, 9961010, 91.0), + (99610, 9961009, 9961010, 90.0), // 京都市営地下鉄烏丸線 五条〜京都: 京都市営地下鉄 GTFS 127本 平均1.3分 (路線値 75km/h) - (99610, 9961010, 9961011, 99.0), + (99610, 9961010, 9961011, 98.0), // 京都市営地下鉄烏丸線 京都〜九条: 京都市営地下鉄 GTFS 127本 平均1.3分 (路線値 75km/h) - (99610, 9961011, 9961012, 95.0), + (99610, 9961011, 9961012, 94.0), // 京都市営地下鉄烏丸線 九条〜十条: 京都市営地下鉄 GTFS 127本 平均1.2分 (路線値 75km/h) - (99610, 9961012, 9961013, 90.0), + (99610, 9961012, 9961013, 88.0), // 京都市営地下鉄烏丸線 十条〜くいな橋: 京都市営地下鉄 GTFS 127本 平均1.4分 (路線値 75km/h) - (99610, 9961013, 9961014, 106.0), + (99610, 9961013, 9961014, 107.0), // 京都市営地下鉄烏丸線 くいな橋〜竹田: 京都市営地下鉄 GTFS 127本 平均1.1分 (路線値 75km/h) - (99610, 9961014, 9961015, 83.0), + (99610, 9961014, 9961015, 84.0), // 京都市営地下鉄東西線 六地蔵〜石田: 京都市営地下鉄 GTFS 112本 平均1.6分 (路線値 65km/h) - (99611, 9961101, 9961102, 58.0), - // 京都市営地下鉄東西線 醍醐〜小野: 京都市営地下鉄 GTFS 112本 平均1.6分 (路線値 65km/h) - (99611, 9961103, 9961104, 71.0), + (99611, 9961101, 9961102, 56.0), + // 京都市営地下鉄東西線 醍醐〜小野: 京都市営地下鉄 GTFS 112本 平均1.5分 (路線値 65km/h) + (99611, 9961103, 9961104, 110.0), // 京都市営地下鉄東西線 小野〜椥辻: 京都市営地下鉄 GTFS 112本 平均1.7分 (路線値 65km/h) (99611, 9961104, 9961105, 74.0), // 京都市営地下鉄東西線 椥辻〜東野: 京都市営地下鉄 GTFS 112本 平均1.4分 (路線値 65km/h) (99611, 9961105, 9961106, 104.0), // 京都市営地下鉄東西線 東野〜山科: 京都市営地下鉄 GTFS 112本 平均1.4分 (路線値 65km/h) - (99611, 9961106, 9961107, 105.0), + (99611, 9961106, 9961107, 104.0), // 京都市営地下鉄東西線 山科〜御陵: 京都市営地下鉄 GTFS 112本 平均2.0分 (路線値 65km/h) - (99611, 9961107, 9961108, 70.0), + (99611, 9961107, 9961108, 84.0), // 京都市営地下鉄東西線 御陵〜蹴上: 京都市営地下鉄 GTFS 154本 平均2.1分 (路線値 65km/h) (99611, 9961108, 9961109, 76.0), + // 京都市営地下鉄東西線 蹴上〜東山: 京都市営地下鉄 GTFS 154本 平均1.4分 (路線値 65km/h) + (99611, 9961109, 9961110, 83.0), // 京都市営地下鉄東西線 東山〜三条京阪: 京都市営地下鉄 GTFS 154本 平均1.1分 (路線値 65km/h) (99611, 9961110, 9961111, 79.0), // 京都市営地下鉄東西線 三条京阪〜京都市役所前: 京都市営地下鉄 GTFS 154本 平均1.0分 (路線値 65km/h) - (99611, 9961111, 9961112, 71.0), + (99611, 9961111, 9961112, 73.0), // 京都市営地下鉄東西線 京都市役所前〜烏丸御池: 京都市営地下鉄 GTFS 154本 平均1.3分 (路線値 65km/h) (99611, 9961112, 9961113, 93.0), // 京都市営地下鉄東西線 烏丸御池〜二条城前: 京都市営地下鉄 GTFS 154本 平均1.3分 (路線値 65km/h) diff --git a/stationapi/src/domain/speed_table.rs b/stationapi/src/domain/speed_table.rs index a30a1c41..0bc23528 100644 --- a/stationapi/src/domain/speed_table.rs +++ b/stationapi/src/domain/speed_table.rs @@ -22,43 +22,46 @@ use crate::model::TrainTypeKind; /// (line_cd, kind, 実効最高速度 km/h)。kind は `TrainTypeKind` の値。 -/// 較正ベンチマーク: 各行のコメントの実所要時間(日中標準)に対し誤差 ±10% 以内。 +/// +/// 距離に線路の長さを使い、GTFS の自動較正を同じ距離で求め直したうえで、それでも +/// 実際の所要時間 (`travel_times/cases.csv` の典型値) から外れる路線・種別だけを +/// 載せる。値は実際の最高速度を超えない範囲で求める。超えないと合わない路線は、 +/// 速度ではなく加減速などのモデル側の問題として扱う。追加・変更したら +/// `make travel-time-report` で全体の誤差が悪くならないことを確かめる。 +/// +/// 距離が直線 × 迂回係数だった頃の値は、距離の水増しを速度で打ち消していたので、 +/// 線路の長さへ替えたときに求め直した。小田急線 (快速急行) は一般則で典型値に +/// 近づいたので外した。つくばエクスプレスと都営大江戸線は GTFS の自動較正に任せる。 +/// オートモード (`trainRoute` の `Legacy`) は求め直す前の値を `legacy_speed_table` +/// で使い続ける。 const LINE_SPEED_OVERRIDES: &[(i32, TrainTypeKind, f64)] = &[ - // 京急本線: 快特・特急 120km/h 運転。品川→横浜 快特 実17分。 - (27001, TrainTypeKind::Express, 120.0), - (27001, TrainTypeKind::LimitedExpress, 120.0), - // 小田急線: 最高 110km/h。新宿→町田 快速急行 実34分。 - (25001, TrainTypeKind::Express, 110.0), + // 総武快速線: 最高 130km/h の別線を走る。StationAPI の路線には快速の停車駅しか + // 無く、通過駅が無いので推定は各停 (Default) として扱う。一般則の 80km/h では + // 遅すぎる。千葉以東の総武本線の普通列車にもかかるが、その区間の基準は無い。 + // travel_times: 錦糸町→津田沼・新小岩→津田沼 快速。 + (11314, TrainTypeKind::Default, 100.0), + // 成田スカイアクセス線: スカイライナーは 160km/h 運転だが、実効値で較正。 + // アクセス特急も同じ LimitedExpress で、京成高砂→成田空港は 39.4 分 + // (実際 38〜52 分。ばらつきが大きく典型値が無いので travel_times には無い)。 + // travel_times: 日暮里→空港第2ビル スカイライナー。 + (23006, TrainTypeKind::LimitedExpress, 120.0), + // 京王井の頭線: 急行の実効速度。travel_times: 渋谷→吉祥寺 急行。 + (24006, TrainTypeKind::Express, 80.0), // 東急東横線: 特急は最高 110km/h だが過密ダイヤ・急曲線で実効は各停並み。 - // 渋谷→横浜 特急 実27分。 + // travel_times: 渋谷→横浜 特急。 (26001, TrainTypeKind::LimitedExpress, 80.0), - // 京王井の頭線: 急行の実効速度。渋谷→吉祥寺 急行 実17分。 - (24006, TrainTypeKind::Express, 80.0), - // 京成本線(都心側): スカイライナーの上野→高砂間の実効速度。 - (23001, TrainTypeKind::LimitedExpress, 100.0), - // 成田スカイアクセス線: スカイライナー 160km/h 運転、アクセス特急 120km/h 運転。 - // 日暮里→空港第2ビル スカイライナー 実36分。 - (23006, TrainTypeKind::LimitedExpress, 160.0), - (23006, TrainTypeKind::Express, 120.0), - // つくばエクスプレス: 各停も 125km/h 運転(駅間が短いため実効値で較正)。 - // 秋葉原→つくば 普通 実66分。快速(kind=HighSpeedRapid)は一般則 120km/h で十分。 - (99309, TrainTypeKind::Default, 95.0), - // 阪急神戸本線: 特急 115km/h 運転。大阪梅田→神戸三宮 実27分。 - (34001, TrainTypeKind::LimitedExpress, 110.0), - // 近鉄特急(名阪甲特急ひのとり): 大阪難波→近鉄名古屋 実125分。 - // 難波線は地下線、大阪線は山間曲線区間を含むため実効値で較正。 + // 京急本線: 快特 (kind は Express)・特急は最高 120km/h。最高速度で快特が + // 典型値 +3.5% (17.6 分) まで近づく。travel_times: 品川→横浜 快特。 + (27001, TrainTypeKind::Express, 120.0), + (27001, TrainTypeKind::LimitedExpress, 120.0), + // 近鉄特急(名阪甲特急ひのとり): 難波線は地下線、大阪線は山間曲線区間を含むため + // 実効値で較正。travel_times には、系統 335 の停車駅が実際の列車と一致しない + // ため入れていない。値は距離を線路の長さへ替える前に決めたもので、見直していない。 (31001, TrainTypeKind::LimitedExpress, 80.0), (31005, TrainTypeKind::LimitedExpress, 115.0), (31027, TrainTypeKind::LimitedExpress, 120.0), - // 都営大江戸線: リニア地下鉄で公表最高速度 70km/h(地下鉄一般則 75km/h より低い)。 - // 都営 GTFS の一様フィットは 60km/h だが、サンプルの大半が 6の字全区間直通 - // (光が丘⇔都庁前 環状部経由・実83〜88分)で、急曲線の多い環状部に引きずられ - // 放射部(落合南長崎→光が丘 実11分)が +14% になるため採用しない。 - // 公表最高速度 70km/h なら全ベンチマーク(放射部 実11分・新宿→光が丘 実24分・ - // 光が丘→都庁前 実22分・清澄白河→赤羽橋 実16分・全区間 実85分)が ±9% 以内。 - // 環状部南側(月島〜赤羽橋の河川横断・急曲線区間)の残差 -8% は路線一様速度では - // 解消できない(区間別較正が必要)。 - (99301, TrainTypeKind::Default, 70.0), + // 阪急神戸本線: 特急の実効速度。travel_times: 大阪梅田→神戸三宮 特急。 + (34001, TrainTypeKind::LimitedExpress, 110.0), ]; /// 公開 GTFS 時刻表からの自動較正エントリ。`scripts/compute_speed_table.py --apply` @@ -66,14 +69,16 @@ const LINE_SPEED_OVERRIDES: &[(i32, TrainTypeKind, f64)] = &[ /// スクリプト側で除外される。手動編集しないこと。 const LINE_SPEED_OVERRIDES_GTFS: &[(i32, TrainTypeKind, f64)] = &[ // --- BEGIN GENERATED (scripts/compute_speed_table.py) --- - // 東京メトロ銀座線 Default: 東京メトロ GTFS 15本 中央値34分 (一般則 75km/h) + // 東京メトロ銀座線 Default: 東京メトロ GTFS 8本 中央値34分 (一般則 75km/h) (28001, TrainTypeKind::Default, 55.0), - // 東京メトロ丸ノ内線 Default: 東京メトロ GTFS 10本 中央値51分 (一般則 75km/h) - (28002, TrainTypeKind::Default, 65.0), + // 東京メトロ丸ノ内線 Default: 東京メトロ GTFS 12本 中央値52分 (一般則 75km/h) + (28002, TrainTypeKind::Default, 60.0), // 東京メトロ日比谷線 Default: 東京メトロ GTFS 7本 中央値45分 (一般則 75km/h) - (28003, TrainTypeKind::Default, 50.0), + (28003, TrainTypeKind::Default, 55.0), // 東京メトロ東西線 Default: 東京メトロ GTFS 20本 中央値54分 (一般則 75km/h) (28004, TrainTypeKind::Default, 60.0), + // 東京メトロ千代田線 Default: 東京メトロ GTFS 13本 中央値42分 (一般則 75km/h) + (28005, TrainTypeKind::Default, 65.0), // 東京メトロ有楽町線 Default: 東京メトロ GTFS 19本 中央値52分 (一般則 75km/h) (28006, TrainTypeKind::Default, 65.0), // 東京メトロ半蔵門線 Default: 東京メトロ GTFS 10本 中央値32分 (一般則 75km/h) @@ -81,19 +86,25 @@ const LINE_SPEED_OVERRIDES_GTFS: &[(i32, TrainTypeKind, f64)] = &[ // 東京メトロ南北線 Default: 東京メトロ GTFS 12本 中央値38分 (一般則 75km/h) (28009, TrainTypeKind::Default, 65.0), // 東京メトロ副都心線 Default: 東京メトロ GTFS 31本 中央値27分 (一般則 75km/h) - (28010, TrainTypeKind::Default, 55.0), + (28010, TrainTypeKind::Default, 60.0), // 東京メトロ副都心線 Express: 東京メトロ GTFS 15本 中央値28分 (一般則 86km/h) - (28010, TrainTypeKind::Express, 60.0), + (28010, TrainTypeKind::Express, 65.0), // 函館市電2系統 Default: 函館市電 GTFS 4本 中央値48分 (一般則 40km/h) (99105, TrainTypeKind::Default, 20.0), // 函館市電5系統 Default: 函館市電 GTFS 6本 中央値47分 (一般則 40km/h) - (99106, TrainTypeKind::Default, 25.0), + (99106, TrainTypeKind::Default, 20.0), + // 都営大江戸線 Default: 都営地下鉄 GTFS 11本 中央値84分 (一般則 75km/h) + (99301, TrainTypeKind::Default, 60.0), // 都営浅草線 Default: 都営地下鉄 GTFS 18本 中央値37分 (一般則 75km/h) (99302, TrainTypeKind::Default, 65.0), // 都営浅草線 LimitedExpress: 都営地下鉄 GTFS 5本 中央値21分 (一般則 90km/h) (99302, TrainTypeKind::LimitedExpress, 55.0), + // 都営新宿線 Default: 都営地下鉄 GTFS 10本 中央値42分 (一般則 75km/h) + (99304, TrainTypeKind::Default, 85.0), // 東京さくらトラム(都電荒川線) Default: 都営地下鉄 GTFS 13本 中央値56分 (一般則 40km/h) (99305, TrainTypeKind::Default, 25.0), + // つくばエクスプレス線 Default: つくばエクスプレス GTFS 11本 中央値46分 (一般則 80km/h) + (99309, TrainTypeKind::Default, 95.0), // 横浜市営地下鉄ブルーライン Rapid: 横浜市営地下鉄 GTFS 3本 中央値61分 (一般則 75km/h) (99316, TrainTypeKind::Rapid, 65.0), // 多摩モノレール Default: 多摩都市モノレール GTFS 6本 中央値38分 (一般則 60km/h) @@ -123,19 +134,16 @@ mod tests { use super::*; #[test] - fn keikyu_express_kinds_are_overridden() { - approx( - line_speed_override_kmh(27001, Some(TrainTypeKind::Express as i32)), - Some(120.0), - ); + fn manual_entry_overrides_only_its_kind() { + // 東急東横線の特急は手動の較正値が引ける。 approx( - line_speed_override_kmh(27001, Some(TrainTypeKind::LimitedExpress as i32)), - Some(120.0), + line_speed_override_kmh(26001, Some(TrainTypeKind::LimitedExpress as i32)), + Some(80.0), ); - // 京急でも各停・快速は一般則にフォールバック。 - assert_eq!(line_speed_override_kmh(27001, None), None); + // 同じ路線でも、エントリの無い種別は一般則にフォールバック。 + assert_eq!(line_speed_override_kmh(26001, None), None); assert_eq!( - line_speed_override_kmh(27001, Some(TrainTypeKind::Rapid as i32)), + line_speed_override_kmh(26001, Some(TrainTypeKind::Rapid as i32)), None ); } @@ -173,8 +181,8 @@ mod tests { // 手動テーブルと同じキーが仮に GTFS テーブルにも存在した場合でも、 // lookup は手動テーブルを先に引くため手動の値が返ることを回帰検知する。 approx( - line_speed_override_kmh(27001, Some(TrainTypeKind::Express as i32)), - Some(120.0), + line_speed_override_kmh(34001, Some(TrainTypeKind::LimitedExpress as i32)), + Some(110.0), ); } diff --git a/stationapi/src/use_case/dto/simulation.rs b/stationapi/src/use_case/dto/simulation.rs index 0acc8cc8..e3716080 100644 --- a/stationapi/src/use_case/dto/simulation.rs +++ b/stationapi/src/use_case/dto/simulation.rs @@ -1,4 +1,4 @@ -use crate::domain::speed_table::line_speed_override_kmh; +use crate::domain::legacy_speed_table::legacy_line_speed_override_kmh; use crate::model::{LineType, TrainTypeKind}; #[derive(Debug, Clone, Copy, PartialEq)] @@ -44,8 +44,9 @@ pub fn resolve_speed_profile( // 路線 × 種別の較正テーブル(実路線の時刻表と公表運転速度に基づく実効値)が // あれば最優先で使う。スカイライナー 160km/h のような路線種別・種別の - // 一般則では表現できない実勢速度をここで反映する。 - if let Some(v_kmh) = line_speed_override_kmh(line_cd, kind) { + // 一般則では表現できない実勢速度をここで反映する。オートモードの走り方を + // 変えないよう、到着時間推定とは別に凍結した表 (legacy_speed_table) を引く。 + if let Some(v_kmh) = legacy_line_speed_override_kmh(line_cd, kind) { return SpeedProfile { max_speed: v_kmh / 3.6, max_acceleration: accel, diff --git a/stationapi/src/use_case/interactor/query.rs b/stationapi/src/use_case/interactor/query.rs index d8f6d041..b745517c 100644 --- a/stationapi/src/use_case/interactor/query.rs +++ b/stationapi/src/use_case/interactor/query.rs @@ -24,8 +24,8 @@ fn filter_to_db_type(filter: TransportTypeFilter) -> Option { use crate::{ domain::{ arrival_estimation::{ - estimate_arrival_minutes_calibrated, is_circular_route, select_circular_arc, - EstimatedStop, EstimationParams, + estimate_arrival_minutes_with_track, is_circular_route, select_circular_arc, + track_distance_key, EstimatedStop, EstimationParams, TrackDistances, }, entity::{ company::Company, @@ -36,13 +36,13 @@ use crate::{ station_number::StationNumber, train_type::TrainType, }, + legacy_speed_table::legacy_segment_speed_override_kmh, normalize::normalize_for_search, repository::{ company_repository::CompanyRepository, line_repository::LineRepository, station_repository::StationRepository, train_type_repository::TrainTypeRepository, }, route_search::{self, JourneySort}, - segment_speed_table::{segment_override_applies_to_kind, segment_speed_override_kmh}, }, model::{self, ConnectedRoute, Route}, use_case::{ @@ -930,7 +930,11 @@ where // 推定は付帯情報を付ける前の駅列で行う (estimateArrivalTimes と同じ入力) let estimated = match route_model { model::TrainRouteModel::Legacy => None, - model::TrainRouteModel::Estimated => estimate_train_route_slice(&sliced, &stations), + model::TrainRouteModel::Estimated => { + let group: Vec<&Station> = stations.iter().collect(); + let track = self.track_distances_of(&group).await?; + estimate_train_route_slice(&sliced, &stations, &track) + } }; let mut segments = self.train_route_segments(sliced, line_group_id).await?; if let Some(estimated) = estimated { @@ -956,6 +960,7 @@ where .get_by_line_group_id(leg.line_group_id) .await?; let group_stops: Vec<&Station> = stations.iter().collect(); + let track = self.track_distances_of(&group_stops).await?; let (from_group, to_group) = (group_of[&leg.from_station_id], group_of[&leg.to_station_id]); let segment = estimate_group_segment( @@ -967,6 +972,7 @@ where }, false, ¶ms, + Some(&track), ) .ok_or_else(|| leg_not_found(leg))?; @@ -1183,11 +1189,13 @@ where let mut result: Vec = Vec::new(); for group_stops in route_row_tree_map.values() { + let track = self.track_distances_of(group_stops).await?; if let Some(segment) = estimate_group_segment( group_stops, SegmentEndpoints::exact(from_station_id, to_station_id), direction_id.is_some(), ¶ms, + Some(&track), ) { result.extend(segment); } @@ -1274,6 +1282,27 @@ where .await } + /// 系統の駅列で隣り合う駅の組 (環状線の継ぎ目の組を含む) の線路の長さを引いて、 + /// 到着時間推定に渡す表にする。推定が切り出す区間は、系統の駅列の連続した区間か + /// 継ぎ目を跨ぐ弧なので、この組だけで足りる。 + async fn track_distances_of(&self, group: &[&Station]) -> Result { + let mut pairs: Vec<(u32, u32)> = group + .windows(2) + .map(|w| (w[0].station_cd as u32, w[1].station_cd as u32)) + .collect(); + if let (Some(first), Some(last)) = (group.first(), group.last()) { + if group.len() > 2 { + pairs.push((last.station_cd as u32, first.station_cd as u32)); + } + } + let distances = self.station_repository.get_track_distances(&pairs).await?; + Ok(pairs + .into_iter() + .zip(distances) + .filter_map(|((a, b), d)| d.map(|d| (track_distance_key(a as i32, b as i32), d))) + .collect()) + } + /// 返す駅の並びで隣り合う 2 駅のあいだの線路の長さを、後ろの駅の /// `track_distance_from_previous` に入れる。先頭の駅は `None` のまま。 /// @@ -1349,11 +1378,15 @@ where resolve_speed_profile(station.line_cd, station.line_type, is_bus, effective_kind); // 隣接駅ペア単位の較正(GTFS 実ダイヤ由来)があれば、このセグメントの // 最高速度を路線単位のプロファイルより優先して上書きする(各停系のみ)。 - if !is_bus && segment_override_applies_to_kind(effective_kind) { + // Legacy はオートモードの走り方を変えないよう、凍結した表を引く。 + if !is_bus { if let Some((_, _, prev_cd)) = prev_stop { - if let Some(v_kmh) = - segment_speed_override_kmh(station.line_cd, prev_cd, station.station_cd) - { + if let Some(v_kmh) = legacy_segment_speed_override_kmh( + station.line_cd, + prev_cd, + station.station_cd, + effective_kind, + ) { profile.max_speed = v_kmh / 3.6; } } @@ -1980,6 +2013,7 @@ fn estimate_group_segment( endpoints: SegmentEndpoints, directed: bool, params: &EstimationParams, + track: Option<&TrackDistances>, ) -> Option> { let mut route_stops: &[&Station] = group_stops; if route_stops.len() > 1 @@ -1990,13 +2024,13 @@ fn estimate_group_segment( let (from, to) = endpoints.positions(route_stops)?; Some(if is_circular_route(route_stops) { let arc = select_circular_arc(route_stops, from, to, directed); - estimate_arrival_minutes_calibrated(&arc, route_stops, params) + estimate_arrival_minutes_with_track(&arc, route_stops, params, track) } else if from < to { - estimate_arrival_minutes_calibrated(&route_stops[from..=to], route_stops, params) + estimate_arrival_minutes_with_track(&route_stops[from..=to], route_stops, params, track) } else { let mut segment: Vec<&Station> = route_stops[to..=from].to_vec(); segment.reverse(); - estimate_arrival_minutes_calibrated(&segment, route_stops, params) + estimate_arrival_minutes_with_track(&segment, route_stops, params, track) }) } @@ -2035,7 +2069,11 @@ fn is_bus_station(station: &Station) -> bool { /// 迂回係数の較正母数には、切り出す前の系統全体 `group` を渡す /// (`estimate_group_segment` と同じく、閉じた環状データの重複終端は除く)。 /// バスの駅を含むときは推定のモデルの対象外として `None` を返す。 -fn estimate_train_route_slice(sliced: &[Station], group: &[Station]) -> Option> { +fn estimate_train_route_slice( + sliced: &[Station], + group: &[Station], + track: &TrackDistances, +) -> Option> { if sliced.iter().any(is_bus_station) { return None; } @@ -2044,10 +2082,11 @@ fn estimate_train_route_slice(sliced: &[Station], group: &[Station]) -> Option 1 && route[0].station_cd == route[route.len() - 1].station_cd { route.pop(); } - Some(estimate_arrival_minutes_calibrated( + Some(estimate_arrival_minutes_with_track( &stops, &route, &EstimationParams::default(), + Some(track), )) } @@ -3198,8 +3237,9 @@ mod tests { /// 直通急行の線内各駅停車の回帰テスト。 /// 半蔵門線内は急行(Express)でも全駅に停車するため、経路スライス内に - /// 通過駅が無ければ各停と同じ推定になること(実所要 押上→神保町 約18分)。 - /// 修正前は種別倍率(×1.15)が掛かり駅間別較正も外れて約15分に縮んでいた。 + /// 通過駅が無ければ各停と同じ推定になること。修正前は種別倍率(×1.15)が + /// 掛かり駅間別較正も外れて短く推定されていた。実際の所要時間との比較は + /// 本番と同じデータで測る所要時間のベンチマーク (travel_times/cases.csv) で行う。 #[tokio::test] async fn test_estimate_route_arrival_times_through_express_all_stops_matches_local() { let default_kind = Some(model::TrainTypeKind::Default as i32); @@ -3226,9 +3266,6 @@ mod tests { l.cumulative_minutes ); } - // 実所要 18 分に対し較正ポリシーの許容誤差 ±10%。 - let total = express_est.last().unwrap().cumulative_minutes; - assert!((16.2..19.8).contains(&total), "got {total}"); } /// 環状部南側(河川横断の急勾配・急曲線区間)の駅間別較正の回帰テスト。 @@ -3794,9 +3831,12 @@ mod tests { .push(station.clone()); } } - Ok(std::sync::Arc::new(RouteNetwork::build( + // get_track_distances と同じ長さで組み立て、見込みと探索の所要時間をそろえる + let distance = self.track_distance; + Ok(std::sync::Arc::new(RouteNetwork::build_with_track( by_line_group.into_values(), &EstimationParams::default(), + move |_, _| distance, ))) } async fn get_track_distances( @@ -4252,6 +4292,31 @@ mod tests { } } + /// 到着時間推定へ渡す線路の長さの表は、系統の隣り合う駅の組に加えて、 + /// 環状線の継ぎ目の組 (末尾→先頭) も持つ。継ぎ目を跨ぐ弧を推定するため。 + #[tokio::test] + async fn track_distances_cover_adjacent_pairs_and_the_seam() { + let stations: Vec = (0..4) + .map(|i| create_test_station(3000 + i, 30, 1, Some(3000))) + .collect(); + let interactor = QueryInteractor { + station_repository: ConfigurableMockStationRepository::new(vec![], vec![]) + .with_track_distance(1234.0), + line_repository: ConfigurableMockLineRepository::new(vec![]), + train_type_repository: ConfigurableMockTrainTypeRepository::new(vec![]), + company_repository: ConfigurableMockCompanyRepository::new(vec![]), + }; + let refs: Vec<&Station> = stations.iter().collect(); + let track = interactor.track_distances_of(&refs).await.unwrap(); + let mut keys: Vec<(i32, i32)> = track.keys().copied().collect(); + keys.sort(); + assert_eq!( + keys, + vec![(3000, 3001), (3000, 3003), (3001, 3002), (3002, 3003)] + ); + assert!(track.values().all(|d| *d == 1234.0)); + } + #[tokio::test] async fn test_update_station_vec_with_attributes_enriches_company_info() { // Create test data @@ -6397,7 +6462,8 @@ mod tests { .all(|s| s.max_acceleration == 0.83 && s.max_deceleration == 0.69)); } - /// Estimated は、同じ駅列を系統全体で較正した到着時間推定の値を返す。 + /// Estimated は、同じ駅列を系統全体で較正し、系統の線路の長さを渡した + /// 到着時間推定の値を返す。 #[tokio::test] async fn estimated_model_follows_the_arrival_estimation() { let group = build_line_group(20); @@ -6410,7 +6476,13 @@ mod tests { let slice: Vec<&Station> = group[2..=6].iter().collect(); let whole: Vec<&Station> = group.iter().collect(); let params = EstimationParams::default(); - let expected = estimate_arrival_minutes_calibrated(&slice, &whole, ¶ms); + let track = interactor.track_distances_of(&whole).await.unwrap(); + assert!( + !track.is_empty(), + "前提: テスト用の repository は線路の長さを返す" + ); + let expected = + estimate_arrival_minutes_with_track(&slice, &whole, ¶ms, Some(&track)); assert_eq!(segments.len(), expected.len()); for (segment, stop) in segments.iter().zip(&expected) { diff --git a/travel_times/baseline.csv b/travel_times/baseline.csv index 0bfaf088..00d69d02 100644 --- a/travel_times/baseline.csv +++ b/travel_times/baseline.csv @@ -1,19 +1,22 @@ label,estimated_minutes -総武快速線 快速 錦糸町→津田沼,22.4690 -総武快速線 快速 新小岩→津田沼,17.3157 -京成 スカイライナー 日暮里→空港第2ビル,42.1152 -京急本線 快特 品川→横浜,17.6754 -小田急小田原線 快速急行 新宿→町田,32.8432 -東急東横線 特急 渋谷→横浜,26.4775 -京王井の頭線 急行 渋谷→吉祥寺,16.8168 -京王線 特急 新宿→京王八王子,41.0112 -阪急神戸本線 特急 大阪梅田→神戸三宮,26.6167 -西武池袋線 準急 池袋→所沢,29.4847 -西武池袋線 急行 池袋→所沢,22.4978 -都営大江戸線 落合南長崎→光が丘,11.5568 -都営大江戸線 新宿→光が丘,24.8605 -都営大江戸線 光が丘→都庁前,22.6198 -都営大江戸線 清澄白河→赤羽橋,16.0367 -都営大江戸線 光が丘→都庁前 (環状部経由),84.3894 -東京メトロ銀座線 渋谷→新橋,13.2494 -つくばエクスプレス 普通 秋葉原→つくば,61.8611 +総武快速線 快速 錦糸町→津田沼,19.9710 +総武快速線 快速 新小岩→津田沼,15.3533 +京成 スカイライナー 日暮里→空港第2ビル,41.0671 +京急本線 快特 品川→横浜,17.6098 +小田急小田原線 快速急行 新宿→町田,29.8999 +東急東横線 特急 渋谷→横浜,26.9109 +京王井の頭線 急行 渋谷→吉祥寺,16.8225 +京王線 特急 新宿→京王八王子,41.3840 +阪急神戸本線 特急 大阪梅田→神戸三宮,26.8621 +西武池袋線 準急 池袋→所沢,29.2534 +西武池袋線 急行 池袋→所沢,22.2665 +都営大江戸線 落合南長崎→光が丘,11.8596 +都営大江戸線 新宿→光が丘,24.9815 +都営大江戸線 光が丘→都庁前,22.7290 +都営大江戸線 清澄白河→赤羽橋,16.2502 +都営大江戸線 光が丘→都庁前 (環状部経由),84.8348 +東京メトロ銀座線 渋谷→新橋,13.2946 +つくばエクスプレス 普通 秋葉原→つくば,61.6267 +東京メトロ半蔵門線 押上→神保町,16.6268 +りんかい線 新木場→大崎,19.1110 +多摩都市モノレール 多摩センター→上北台,37.6339 diff --git a/travel_times/cases.csv b/travel_times/cases.csv index 4f722854..48a2e125 100644 --- a/travel_times/cases.csv +++ b/travel_times/cases.csv @@ -17,3 +17,6 @@ label,line_group_id,from_station_id,to_station_id,slice_end_station_id,measure,r 都営大江戸線 光が丘→都庁前 (環状部経由),1000099301,9930138,9930100,,arrival,84,86,84,東京都交通局 GTFS 東京メトロ銀座線 渋谷→新橋,1137,2800119,2800112,,arrival,15,15,15,東京メトロ GTFS つくばエクスプレス 普通 秋葉原→つくば,977,9930901,9930920,,arrival,63,66,66,首都圏新都市鉄道 GTFS +東京メトロ半蔵門線 押上→神保町,656,2800813,2800807,,arrival,17,19,17,東京メトロ GTFS +りんかい線 新木場→大崎,170,9933701,9933708,,arrival,19,20,19,東京臨海高速鉄道 GTFS +多摩都市モノレール 多摩センター→上北台,1000099334,9933401,9933419,,arrival,37,39,37,多摩都市モノレール GTFS From bac7f10fca8db39d968458e5b0bb29fc6554d728 Mon Sep 17 00:00:00 2001 From: Tsubasa SEKIGUCHI Date: Thu, 1 Oct 2026 04:44:05 +0900 Subject: [PATCH 4/5] =?UTF-8?q?=E7=B7=9A=E8=B7=AF=E3=81=AE=E9=95=B7?= =?UTF-8?q?=E3=81=95=E3=81=A8=E6=B1=82=E3=82=81=E7=9B=B4=E3=81=97=E3=81=9F?= =?UTF-8?q?=E8=BC=83=E6=AD=A3=E3=82=92trainRoute=E3=81=AEEstimated?= =?UTF-8?q?=E3=81=A0=E3=81=91=E3=81=A7=E4=BD=BF=E3=81=84=E3=80=81estimateA?= =?UTF-8?q?rrivalTimes=E3=81=A8connectedRoutes=E3=82=92=E5=85=83=E3=81=AE?= =?UTF-8?q?=E8=A8=88=E7=AE=97=E3=81=AB=E6=88=BB=E3=81=97=E3=81=9F=20(#1714?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Claude Opus 5.5 --- AGENTS.md | 4 +- docs/architecture.md | 27 +-- scripts/README.md | 4 +- scripts/travel_time_report.py | 20 ++- src/repository.rs | 50 +++--- src/travel_times.rs | 27 +-- stationapi/src/domain/arrival_estimation.rs | 45 ++++- stationapi/src/domain/legacy_speed_table.rs | 15 +- stationapi/src/domain/segment_speed_table.rs | 3 + stationapi/src/domain/speed_table.rs | 9 +- stationapi/src/use_case/interactor/query.rs | 163 +++++++++++-------- travel_times/README.md | 9 +- 12 files changed, 229 insertions(+), 147 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 39922b06..fee7d1de 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -71,14 +71,14 @@ The Worker is the workspace root package. `stationapi`, `preprocessor`, and `dat - **Stations** – `station`, `stations`, `stationGroupStations`, `stationsNearby`, `lineStations`, `stationsByName`, `lineGroupStations`, `lineListStations`, `lineGroupListStations`. `QueryInteractor` enriches stations with lines, companies, station numbers, and train types. `lineStations` resolves the line's local train-type group (rail `kind` 0/1 or a `priority > 0` type; a bullet-train line, which has no local service, takes the group of its stopping train type with the smallest `types.id`, e.g. Nozomi or Hayabusa) and returns its stops carrying that group's train type, with or without `stationId`, so a client that never picked a train type still has the `lineGroupId` `trainRoute` requires; when no such group exists — bus lines only carry `BusRoute` (`kind` 7, `priority` 0) variants — it falls back to the line's plain typeless station list so bus stop listings never return empty. `stationsByName` with `fromStationGroupId` returns the stations reachable from there: stations sharing a line group with the origin (`line_group_cd` set, `has_train_types` true), same-line stations when either side has no line group, and — for rail — stations reachable by transferring, i.e. those for which `connectedRoutes` with `viaLineId` set to the station's line returns a route (`line_group_cd` empty, `has_train_types` false). The transfer check uses `RouteTopology` (`stationapi/src/domain/route_topology.rs`), a time-free copy of the `connectedRoutes` network built straight from the index without `Station` entities or time estimates (about 20 ms instead of about 190 ms, cached in its own `OnceLock`); `RouteNetwork` holds the same topology, both share `trim_pattern` and `line_group_rows`, and a real-data test asserts the two are equal. The check is a ride-limited BFS over line groups (a few ms) plus, only for destinations that are cut vertices of the station–line-group graph, a check that arrives without stopping over at the destination group — otherwise a branch's junction station (Ishibashi-handai-mae on the Minoo Line) would be listed although reaching it on that branch means riding out and back. - **Lines** – `line`, `lines`, `linesByName`. Results include company data and computed line symbols based on repository helpers. - **Routes** – `routes`, `connectedRoutes`, `estimateArrivalTimes`, `trainRoute`. Paging tokens are currently empty (pagination not implemented). -- **`trainRoute`** – Takes the line group's stops from the repository *before* any enrichment, slices them to the requested `fromStationId`–`toStationId` range (reversing when the request runs backwards), and only then attaches lines, companies, station numbers, train types, and nearby bus routes. Enrichment is per-station and independent, so slicing first does not change any segment; enriching the whole line group first made a three-station request cost the same as a 250-station one. Keep the order — the cost of this query must stay proportional to the requested range, not to the line group. The optional `model` argument (`TrainRouteModel`) picks how the segment values are computed. `Legacy`, the default, keeps the values from when the query was added (#1568): `dto::simulation::resolve_speed_profile` supplies the speed and acceleration, and `arrivalCumulativeMinutes` / `departureCumulativeMinutes` stay `null`. TrainLCD/MobileApp's auto mode depends on these values, so do not change them; `Legacy` reads frozen copies of the speed tables (`domain/legacy_speed_table.rs`) so that recalibrating the arrival estimation does not move it. `Estimated` runs `arrival_estimation` — the model the speed tables are calibrated against — on the same pre-enrichment slice (with `legs`, it reuses `estimate_connected_route_arrival_times`, so the values equal `estimateArrivalTimes` given the same `legs`). It then replaces each segment's `stops`, speed, and acceleration with the estimator's and fills the two cumulative fields. A route containing a bus station keeps the `Legacy` values, because the estimator's kinematics do not apply to buses. +- **`trainRoute`** – Takes the line group's stops from the repository *before* any enrichment, slices them to the requested `fromStationId`–`toStationId` range (reversing when the request runs backwards), and only then attaches lines, companies, station numbers, train types, and nearby bus routes. Enrichment is per-station and independent, so slicing first does not change any segment; enriching the whole line group first made a three-station request cost the same as a 250-station one. Keep the order — the cost of this query must stay proportional to the requested range, not to the line group. The optional `model` argument (`TrainRouteModel`) picks how the segment values are computed. `Legacy`, the default, keeps the values from when the query was added (#1568): `dto::simulation::resolve_speed_profile` supplies the speed and acceleration, and `arrivalCumulativeMinutes` / `departureCumulativeMinutes` stay `null`. TrainLCD/MobileApp's auto mode depends on these values, so do not change them; `Legacy` reads frozen copies of the speed tables (`domain/legacy_speed_table.rs`) so that recalibrating `Estimated` does not move it. `Estimated` runs `arrival_estimation` — the model the speed tables are calibrated against — on the same pre-enrichment slice (with `legs`, it reuses `estimate_connected_route_arrival_times`, so the values equal `estimateArrivalTimes` given the same `legs`). It then replaces each segment's `stops`, speed, and acceleration with the estimator's and fills the two cumulative fields. A route containing a bus station keeps the `Legacy` values, because the estimator's kinematics do not apply to buses. - **Coordinate lookups** – `index::nearest` (k nearest, used by `stationsNearby`) and `index::within_radius` (everything inside a radius, used by the nearby-bus-stop enrichment) both go through a per-transport-type grid index (`Grid`, CSR over 0.05° cells) instead of scanning the whole station table. `nearest` searches a radius, widens it while fewer than `limit` stations fall inside, and stops once the radius covers the index — anything outside a radius that already holds `limit` hits cannot be in the top `limit`. With `transportType` omitted it returns rail stations first and bus stops after, each group sorted by distance — the pre-Workers SQL's `ORDER BY transport_type, distance`. The limit applies to the merged order, so `nearest` fills it with rail and only asks the bus grid for the remaining slots; a location with `limit` rail stations returns no bus stops at all. Ties on distance break on `station_cd` so the order does not depend on an unstable sort. Every station lookup by coordinates runs on every request that enriches rail stations with nearby bus routes, so keep new coordinate queries on the grid rather than adding another full scan. - **Train types** – `stationTrainTypes`, `routeTypes`. Train types aggregate by line group and include related lines plus optional train type metadata. Rail variants use `TrainTypeKind::{Default, Branch, Rapid, Express, LimitedExpress, HighSpeedRapid, CommuterRapid}` (0-6); bus variants use `BusRoute` (7), which represents a `(route_id, shape_id)` operation pattern (e.g. 循環 / 短ターン / 支線) generated automatically from the configured GTFS bus feeds (Toei Bus, Seibu Bus, Keio Bus) and the converted Tokyu Bus JSON. - **Default rail train types** – `preprocessor` fills every active rail line containing at least one station with no `station_station_types` row with a deterministic, complete all-stop group. The generated rows exist only in `generated/*.csv`; canonical CSV files remain unchanged. `type_cd=100` represents 「普通」 and `type_cd=101` represents 「各駅停車」. An existing 100/101 assignment on the line takes precedence; otherwise the label is selected per line through `LOCAL_SERVICE_RAIL_LINE_IDS` in `preprocessor/src/rail.rs`. Generated `line_group_cd` values use `1,000,000,000 + line_cd`; generation fails on a collision. Bus lines are excluded and continue to use their GTFS-derived `BusRoute` groups. - **GTFS bus integration** – `preprocessor/src/gtfs/` reads the GTFS feeds into an in-memory representation and then projects them onto the shared `stations` / `lines` / `types` / `station_station_types` tables (`gtfs/integrate.rs`). Only routes, stops, trips, and stop_times are read; calendar, shapes, feed_info, and agencies do not affect the output. Every configured GTFS feed is imported, including Seibu Bus and Keio Bus (both downloaded from ODPT with `ODPT_ACCESS_TOKEN`). Tokyu Bus ordinary-route `BusroutePattern`, `BusstopPole`, and `BusTimetable` JSON are converted into the same representation; pattern IDs become `shape_id` values so route variants remain queryable as bus TrainTypes. The Tokyu-operated Ota, Shinagawa, and Meguro community buses use their official GTFS feeds and matching JSON routes are excluded to prevent duplicates. `ODPT_ACCESS_TOKEN` is required for authenticated sources; without it those feeds are skipped with a warning rather than failing the build. Stops whose Tokyu JSON records omit coordinates remain available to name and route queries but not coordinate searches. `transport_type` (0: rail, 1: bus) on both `stations` and `lines` keeps rail and bus records queryable side by side. GTFS IDs are namespaced per feed before import to avoid cross-operator collisions. `line_cd` (100,000,000+), `station_cd` / `station_g_cd` (200,000,000+), and bus `type_cd` / `line_group_cd` (100,000,000+) are all deterministic fnv1a hashes that stay clear of the rail data ranges. Disable the entire bus pipeline with `DISABLE_BUS_FEATURE=true`. - **Bus stop translations (readings & English)** – GTFS-JP `translations.txt` layouts differ per feed, so `load_translations` (`preprocessor/src/gtfs/parse.rs`) resolves columns by header name (Seibu ships 6 columns without `record_sub_id`; Keio and the Tokyu community feeds ship 7) and indexes each `stop_name` translation under both keys it may use: `record_id` (== the stop_id, Seibu — with the "-NN" pole suffix also mapped to the parent stop_id) and `field_value` (== the Japanese stop_name, Keio / Tokyu community, where `record_id` is left empty). `load_stops` then looks a stop's translation up by stop_id first, then by name. Keying only by `record_id` would silently drop every field_value-keyed feed, leaving `station_name_k` filled with the kanji stop_name and `station_name_r` empty. Readings arriving as half-width katakana (`ニシハチオウジ`, Keio / Tokyu community) are folded to full-width via `romaji::to_fullwidth_katakana()` before storage. - **Bus English-name fallback** – When a feed provides no English (`en`) translation for a stop — e.g. Tokyu Bus ordinary-route JSON, which carries only `dc:title` and `odpt:kana` — `stationapi/src/domain/romaji.rs::romaji_display_name()` derives a modified-Hepburn romanization (with macrons for long vowels, matching the curated rail style: Tōkyō / Kyōto / Shin-Ōsaka) from the kana reading, and the GTFS reader fills `stop_name_r` with it. The fallback never overwrites a real `en` value, and a reading with no convertible kana stays `NULL` rather than emitting a partial transcription. Because `stop_name_r` is the single upstream source that fans out into the `stations` projection, `search_by_name`, and the romanized bus route/headsign names, this supplements every English-facing surface at once. When projecting into `stations`, `station_name_rn` is filled with the plain-ASCII spelling via `romaji::strip_macrons()` (Tōkyō → Tokyo), mirroring the rail dataset's `_r` (macron) / `_rn` (macron-free) column pair. -- **Track distances** – `Station.trackDistanceFromPrevious` is the track length in meters from the station before it in the returned list, for clients that total a ride's distance (TrainLCD/MobileApp's ride log). It is distinct from `trainRoute`'s `distanceFromPrevious`, which stays the straight-line distance the app's running simulation relies on. The arrival estimation (`estimateArrivalTimes`, `trainRoute` with `Estimated`, and the ride times of `connectedRoutes`) uses the track length as the running distance wherever one exists and falls back to straight line × detour factor elsewhere; `scripts/compute_speed_table.py` fits the speed tables with the same distances, so it needs `generated/` from `make data`. Only `lineStations`, `lineGroupStations`, `trainRoute`, and `stations(ids)` fill it (`attach_track_distances` in `QueryInteractor`, called once the order is final). `stations(ids)` returns the stations in the order of `ids` — the repository keeps that order and enrichment never reorders — so a client passing a route's station IDs (MobileApp's `sids` deep links) gets the lengths between IDs adjacent in that order; a pair that is not adjacent in the track data (an ID list skipping stations) is `null`. The first station, the first station of each `trainRoute` leg, sections without N02 geometry, and every other query return `null`, and clients fall back to the straight line there. `preprocessor/src/track/` builds an undirected graph from N02's `RailroadSection` LineStrings, collects every pair adjacent in a line's `(e_sort, station_cd)` order or a line group's `station_station_types.id` order (with and without closed stations, plus the seam of loop services within 3 km), snaps each station to every track within 500 m, and takes the Dijkstra path minimizing `2 × snap offset + track length` — snapping to the nearest track alone picks another line of the same operator at large stations (Honmachi) and detours through a transfer station. It first measures on the operators of both stations' companies (`OPERATOR_ALIASES` maps companies whose name differs from N02's), then retries on every operator for lines running on another company's track; a path longer than max(3 × straight, straight + 5 km) counts as unmeasured, and a result shorter than the straight line is raised to it. Pairs in one station group are 0. `build.rs` writes the sorted pairs to `connections.bin`, and `index::track_distance` binary-searches it, so no index is built at isolate start. When bumping the N02 edition, change the URL and cache directory in `preprocessor/src/track/mod.rs` and the `Cache N02 railway data` step in `.github/actions/build-worker/action.yml` together and compare the unmeasured count in the `preprocessor` log. `docs/architecture.md` (駅間の線路の長さ) has the details. +- **Track distances** – `Station.trackDistanceFromPrevious` is the track length in meters from the station before it in the returned list, for clients that total a ride's distance (TrainLCD/MobileApp's ride log). It is distinct from `trainRoute`'s `distanceFromPrevious`, which stays the straight-line distance the app's running simulation relies on. Only `trainRoute` with `Estimated` (used by TrainLCD/MobileApp's GPX generation) uses the track length as the running distance (straight line × detour factor where none exists) together with the recalibrated speed tables (`speed_table` / `segment_speed_table`, `SpeedCalibration::Recalibrated`); `scripts/compute_speed_table.py` fits those tables with the same distances, so it needs `generated/` from `make data`. Everything else — `estimateArrivalTimes`, the ride times of `connectedRoutes`, and `trainRoute` with `Legacy` — keeps the original estimation (straight line × detour factor and the frozen tables in `domain/legacy_speed_table.rs`, the `EstimationParams` default), so recalibrating never moves ETAs, route search results, or auto mode. Only `lineStations`, `lineGroupStations`, `trainRoute`, and `stations(ids)` fill it (`attach_track_distances` in `QueryInteractor`, called once the order is final). `stations(ids)` returns the stations in the order of `ids` — the repository keeps that order and enrichment never reorders — so a client passing a route's station IDs (MobileApp's `sids` deep links) gets the lengths between IDs adjacent in that order; a pair that is not adjacent in the track data (an ID list skipping stations) is `null`. The first station, the first station of each `trainRoute` leg, sections without N02 geometry, and every other query return `null`, and clients fall back to the straight line there. `preprocessor/src/track/` builds an undirected graph from N02's `RailroadSection` LineStrings, collects every pair adjacent in a line's `(e_sort, station_cd)` order or a line group's `station_station_types.id` order (with and without closed stations, plus the seam of loop services within 3 km), snaps each station to every track within 500 m, and takes the Dijkstra path minimizing `2 × snap offset + track length` — snapping to the nearest track alone picks another line of the same operator at large stations (Honmachi) and detours through a transfer station. It first measures on the operators of both stations' companies (`OPERATOR_ALIASES` maps companies whose name differs from N02's), then retries on every operator for lines running on another company's track; a path longer than max(3 × straight, straight + 5 km) counts as unmeasured, and a result shorter than the straight line is raised to it. Pairs in one station group are 0. `build.rs` writes the sorted pairs to `connections.bin`, and `index::track_distance` binary-searches it, so no index is built at isolate start. When bumping the N02 edition, change the URL and cache directory in `preprocessor/src/track/mod.rs` and the `Cache N02 railway data` step in `.github/actions/build-worker/action.yml` together and compare the unmeasured count in the `preprocessor` log. `docs/architecture.md` (駅間の線路の長さ) has the details. - **TTS metadata** – `Station`, `StationNested`, `Line`, `LineNested`, `TrainType`, and `TrainTypeNested` expose `name_ipa` / `name_roman_ipa` plus `name_tts_segments` for multi-segment pronunciation output. Use `name_tts_segments` when clients need per-token SSML construction for mixed-language names such as `Kasai-Rinkai Park`. - **Connected routes** – `connectedRoutes` finds transfer routes automatically, like a journey planner, using a frequency-based RAPTOR search in `stationapi/src/domain/route_search.rs`. Each rail line group is a pattern, station groups are the transfer nodes, and ride times come from `arrival_estimation`; bus lines are excluded. The cost adds a per-boarding wait by `TrainTypeKind` (limited express 15 min, express / high-speed rapid 5 min, others 3 min) and a 3-minute transfer walk — without the wait, infrequent limited expresses would beat the Yamanote Line. Rounds give the time/transfer Pareto set; alternatives come from re-searching with one leg's parallel line groups banned along that leg (at most 8 searches), and are dropped beyond 1.15 × best + 15 min or with two more transfers than the Pareto set. Alternative routes that stop at the same station group in two different legs (backtracking to re-board a banned train) are dropped; pass-through stations are not counted, and the Pareto routes of the first search are never dropped this way (otherwise a station `stationsByName` reports as reachable could get no route). Results are ranked by cost + 5 min per transfer and capped at 6. The time and transfer count stay internal (the API does not return them), so ordering happens on the server: `sortBy: ConnectedRouteSort` picks `Recommended` (the ranking above, the default when omitted), `ArrivalTime` (the estimated time `estimateArrivalTimes` also reports — excluding the first train's wait — then fewer transfers), or `TransferCount` (fewer transfers, then earlier arrival). `route_search::sort_journeys` only reorders the set `search` returned, with a stable sort so ties keep the recommended order; the set itself never depends on `sortBy`. The network (every rail line group plus its time estimates, about 190 ms natively) is built lazily into a `OnceLock` by `StationRepository::get_route_network` on the first `connectedRoutes` call, so other queries never pay for it. Each route is a list of `legs` shaped for the app's one-train-at-a-time flow: every leg carries its boarding and alighting `Station`, both on the line of the line group the search rode — so at a transfer the previous leg's alighting station and the next leg's boarding station may be different stations of one station group — `stationGroupIds`, the station groups from boarding to alighting in travel order including pass-through stations (the search's `JourneyLeg.station_group_ids` as-is — station groups rather than station IDs so the client can match them against whichever train type it picks, which may run on another line; a group appears twice on patterns such as the Oedo Line's Tochomae), and `trainTypes`, every train type usable on that leg (real `groupId`s, so the client picks one and calls `lineGroupStations`): it is exactly `routeTypes(boarding station group, alighting station group, alighting station's line)` (same dedup, same `lines`, same order — the use case calls `get_train_types`), because the search collapses parallel services such as local and rapid into one route and the app needs them to list types and default to the local. `viaLineId`, like `routeTypes`, is the line of the tapped search result and keeps only routes whose last leg arrives on that line. `estimateArrivalTimes` and `trainRoute` accept `legs: [RouteLegInput!]` (the `groupId` of the train type picked from each leg's `trainTypes`, plus the leg's `fromStation.id` and `toStation.id`) and then return values for the whole transfer route. A leg endpoint missing from the chosen line group is matched by station group (the picked local may stop at another line's station of the same group), preferring an exact `station_cd` and, among same-group candidates, the pair giving the shortest slice (through services list two stations of a junction group). ETA estimates each leg on its own line group only and chains them from the origin, adding the 3-minute walk and the next train type's wait at each transfer (the same allowance the search ranks by), returning one route with an empty `id`; `trainRoute` concatenates each leg's segments (each leg restarts at distance 0). Both slice legs with the same function, taking the shorter arc on loop lines, so their station sequences match. More than `MAX_RIDES` (6) legs — more than `connectedRoutes` ever returns — legs that do not connect, ends that differ from `fromStationId` / `toStationId`, or combining `legs` with `viaLineIds` / `directionId` / `lineGroupId` are errors. `docs/architecture.md` (乗換経路探索) has the details, and `docs/route-search.md` documents the search internals (data structures, the scan, pruning, alternatives, determinism). - Changes to the published contract require coordinated updates to `schema/public.graphql`, the async-graphql types in `src/graphql/`, and, when the shape of a value changes, `stationapi/src/model.rs` and the DTO conversions. diff --git a/docs/architecture.md b/docs/architecture.md index 698fe482..cf821dd8 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -484,17 +484,20 @@ input RouteLegInput { lineGroupId: Int! fromStationId: Int! toStationId: Int! MobileApp のオートモードがこの値で走るので、値を変えません。速度の較正は、 到着時間推定の較正を求め直す前の表を `domain/legacy_speed_table.rs` に凍結して 使います。 -- `Estimated`: 到着時間推定 (`arrival_estimation`) のモデルです。速度の較正 - テーブルは、このモデルに当てはめて求めています。返す駅列に推定を掛け、停車・ - 通過、最高速度、加減速を推定が使った値に置き換えて、到着・出発の見込みを入れ - ます。`legs` を渡したときの見込みは、同じ `legs` を渡した - `estimateArrivalTimes` と同じ値です。バスの駅を含む経路は推定のモデルの対象外 - なので、`Legacy` と同じ値を返します。 +- `Estimated`: 到着時間推定 (`arrival_estimation`) のモデルで、MobileApp の GPX の + 生成が使います。返す駅列に推定を掛け、停車・通過、最高速度、加減速を推定が + 使った値に置き換えて、到着・出発の見込みを入れます。`legs` を渡したときの見込みは、 + 同じ `legs` を渡した `estimateArrivalTimes` と同じ組み立て方 (乗換の徒歩と待ち + 時間) ですが、較正と駅間の距離が違うので値は一致しません (下記)。バスの駅を含む + 経路は推定のモデルの対象外なので、`Legacy` と同じ値を返します。 2 つのモデルは、加減速、運転余裕率、停車時間、較正テーブル、駅間の距離が違い -ます。到着時間推定は、線路の長さ (`connections`) がある駅間ではそれを走行距離に -使い、無い駅間だけ直線距離 × 迂回係数で見積もります。乗換経路探索 -(`connectedRoutes`) の所要時間も同じ距離で求めます。`Legacy` の値で台形の速度プロファイルを作って走らせると、 +ます。`Estimated` は、線路の長さ (`connections`) がある駅間ではそれを走行距離に +使い、無い駅間だけ直線距離 × 迂回係数で見積もり、その距離で求め直した較正 +(`speed_table` / `segment_speed_table`) を使います。この求め直した計算を使うのは +`Estimated` だけです。`estimateArrivalTimes` と乗換経路探索 (`connectedRoutes`) は、 +元の計算 (直線距離 × 迂回係数と、`domain/legacy_speed_table.rs` の元の較正) の +ままで、ETA と経路検索の結果は変わりません。`Legacy` の値で台形の速度プロファイルを作って走らせると、 `estimateArrivalTimes` より短い時間で走り切ります (#1709)。所要時間を推定に 合わせたいクライアントは、`Estimated` の見込みを使います。 @@ -512,8 +515,8 @@ input RouteLegInput { lineGroupId: Int! fromStationId: Int! toStationId: Int! ### 所要時間のベンチマーク (`travel_times/`) -到着時間推定の所要時間を、実際の列車の所要時間と比べる基準を `travel_times/cases.csv` -に置いています。速度の較正テーブルや一般則は、1 つの路線に合わせて変えると、同じ +`trainRoute` の `Estimated` の所要時間を、実際の列車の所要時間と比べる基準を +`travel_times/cases.csv` に置いています。速度の較正テーブルや一般則は、1 つの路線に合わせて変えると、同じ 規則を使うほかの路線の推定も変わります。変更の前後で全体の誤差を測るための仕組み です。 @@ -764,7 +767,7 @@ repository の実装がないメソッドは、空の結果ではなく `DomainE │ │ ├── entity/ # Station / Line / TrainType / Company ... │ │ ├── repository/ # 抽象インターフェース │ │ ├── arrival_estimation.rs -│ │ ├── legacy_speed_table.rs # trainRoute の Legacy (オートモード) が使う凍結した較正 +│ │ ├── legacy_speed_table.rs # 元の較正 (Estimated 以外のすべてが使う) │ │ ├── route_search.rs # 乗換経路探索 (RAPTOR) │ │ ├── route_topology.rs # 所要時間を持たない系統網 (stationsByName の到達判定) │ │ ├── segment_speed_table.rs diff --git a/scripts/README.md b/scripts/README.md index 3b45211d..6c05e844 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -48,7 +48,9 @@ OSM データは [Open Database License (ODbL)](https://www.openstreetmap.org/co ## compute_speed_table.py 公開 GTFS 時刻表から、到着時間推定(`arrival_estimation.rs`)の速度較正テーブル -2 種類を再計算します。運動学モデルを Python で再現し、実ダイヤの所要時間を +2 種類を再計算します。このテーブルを使うのは `trainRoute` の `Estimated` だけです +(`estimateArrivalTimes` と `connectedRoutes` は `legacy_speed_table.rs` の元の較正を +使います)。運動学モデルを Python で再現し、実ダイヤの所要時間を 再現する実効最高速度を二分探索でフィッティングします。 駅間の距離は、推定と同じく線路の長さ(`generated/connections.csv`)を使い、無い diff --git a/scripts/travel_time_report.py b/scripts/travel_time_report.py index dd9fc437..db13b74a 100644 --- a/scripts/travel_time_report.py +++ b/scripts/travel_time_report.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -"""実際の所要時間 (travel_times/cases.csv) に対する到着時間推定の誤差を、動いている -Worker に問い合わせて Markdown で出す。 +"""実際の所要時間 (travel_times/cases.csv) に対する trainRoute の Estimated (MobileApp の +GPX の生成が使う推定) の誤差を、動いている Worker に問い合わせて Markdown で出す。 CI の回帰テスト (src/travel_times.rs) は data/*.csv だけで動くので、生成データにしか 無い種別グループを飛ばし、線路の長さも持たない。本番と同じ生成データでの精度は、 @@ -25,9 +25,9 @@ # 既定の Python-urllib は配信側で弾かれるので、bench.py と同じく名乗る USER_AGENT = "stationapi-travel-time-report/1.0 (+https://github.com/TrainLCD/StationAPI)" QUERY = """query TravelTimeReport($from: Int!, $to: Int!, $group: Int!) { - estimateArrivalTimes(fromStationId: $from, toStationId: $to, + trainRoute(fromStationId: $from, toStationId: $to, model: Estimated, legs: [{ lineGroupId: $group, fromStationId: $from, toStationId: $to }]) { - routes { stops { stationId cumulativeMinutes departureCumulativeMinutes } } + segments { station { id } arrivalCumulativeMinutes departureCumulativeMinutes } } }""" @@ -49,11 +49,15 @@ def estimate(api: str, case: dict) -> float: data = json.load(res) if data.get("errors"): raise RuntimeError("; ".join(e.get("message", "") for e in data["errors"])) - stops = data["data"]["estimateArrivalTimes"]["routes"][0]["stops"] + segments = data["data"]["trainRoute"]["segments"] target = int(case["to_station_id"]) - stop = next(s for s in stops[1:] if s["stationId"] == target) - key = "cumulativeMinutes" if case["measure"] == "arrival" else "departureCumulativeMinutes" - return float(stop[key]) + segment = next(s for s in segments[1:] if s["station"]["id"] == target) + key = ( + "arrivalCumulativeMinutes" + if case["measure"] == "arrival" + else "departureCumulativeMinutes" + ) + return float(segment[key]) def range_error(est: float, lo: float, hi: float) -> float: diff --git a/src/repository.rs b/src/repository.rs index 2f82aa0c..3171df5e 100644 --- a/src/repository.rs +++ b/src/repository.rs @@ -158,10 +158,9 @@ fn route_network() -> &'static Arc { } fn build_route_network() -> RouteNetwork { - RouteNetwork::build_with_track( + RouteNetwork::build( rail_line_group_cds().map(|group| stations_of_line_groups(&[group as u32])), &EstimationParams::default(), - index::track_distance, ) } @@ -1667,9 +1666,11 @@ mod tests { assert_eq!(route_ids, ids); } - /// Estimated の trainRoute は、同じ legs を渡した estimateArrivalTimes と同じ - /// 見込みを返し、停車・加減速も推定のモデルにそろう。Legacy は見込みを返さず、 - /// 区間の値も変わらない。 + /// Estimated の trainRoute は、同じ legs を渡した estimateArrivalTimes と同じ駅を + /// 同じ組み立て方 (乗換の徒歩と待ち時間) でつないだ見込みを返し、停車・加減速も + /// 推定のモデルにそろう。値は Estimated だけが求め直した較正と線路の長さを使う + /// ので、estimateArrivalTimes とは一致しない。Legacy は見込みを返さず、区間の値も + /// 変わらない。 #[test] fn estimated_connected_train_route_carries_the_estimate_arrival_times() { use stationapi::use_case::traits::query::QueryUseCase; @@ -1689,17 +1690,17 @@ mod tests { .unwrap(); assert_eq!(estimated.len(), eta.len()); + let mut previous_departure = 0.0; for (i, (segment, stop)) in estimated.iter().zip(&eta).enumerate() { assert_eq!( - segment.arrival_cumulative_minutes, - Some(stop.cumulative_minutes) - ); - assert_eq!( - segment.departure_cumulative_minutes, - Some(stop.departure_cumulative_minutes) + segment.station.as_ref().map(|station| station.id as i32), + Some(stop.station_cd) ); + let arrival = segment.arrival_cumulative_minutes.unwrap(); + let departure = segment.departure_cumulative_minutes.unwrap(); + assert!(arrival >= previous_departure - 1e-9 && departure >= arrival); + previous_departure = departure; assert_eq!(segment.stops, stop.stops_here); - assert_eq!(segment.max_speed, stop.max_speed_kmh / 3.6); assert_eq!(segment.max_acceleration, params.accel); assert_eq!(segment.max_deceleration, params.decel); // 駅と距離は Legacy と同じ @@ -1718,7 +1719,7 @@ mod tests { } /// lineGroupId で呼んだ Estimated の trainRoute は、その区間を 1 つの leg にした - /// estimateArrivalTimes と同じ見込みになる (環状線でない系統では同じ駅列を + /// Estimated の trainRoute と同じ見込みになる (環状線でない系統では同じ駅列を /// 同じ較正母数で推定するため)。#1709 の 2 経路で確かめる。 #[test] fn estimated_train_route_matches_a_single_leg_estimate() { @@ -1733,27 +1734,26 @@ mod tests { TrainRouteModel::Estimated, )) .unwrap(); - let eta = block_on(interactor.estimate_connected_route_arrival_times(&[ - model::RouteLegRequest { + let single_leg = block_on(interactor.get_connected_train_route( + &[model::RouteLegRequest { line_group_id, from_station_id: from, to_station_id: to, - }, - ])) + }], + TrainRouteModel::Estimated, + )) .unwrap(); - assert_eq!(segments.len(), eta.len()); - for (segment, stop) in segments.iter().zip(&eta) { - assert_eq!( - segment.station.as_ref().map(|station| station.id as i32), - Some(stop.station_cd) - ); + assert_eq!(segments.len(), single_leg.len()); + for (segment, leg_segment) in segments.iter().zip(&single_leg) { + assert_eq!(segment.station, leg_segment.station); + assert!(segment.arrival_cumulative_minutes.is_some()); assert_eq!( segment.arrival_cumulative_minutes, - Some(stop.cumulative_minutes) + leg_segment.arrival_cumulative_minutes ); assert_eq!( segment.departure_cumulative_minutes, - Some(stop.departure_cumulative_minutes) + leg_segment.departure_cumulative_minutes ); } } diff --git a/src/travel_times.rs b/src/travel_times.rs index 1c6aeff3..231ca0c1 100644 --- a/src/travel_times.rs +++ b/src/travel_times.rs @@ -1,4 +1,6 @@ -//! 実際の所要時間 (`travel_times/cases.csv`) に対する到着時間推定の回帰の見張り。 +//! 実際の所要時間 (`travel_times/cases.csv`) に対する `trainRoute` の `Estimated` +//! (MobileApp の GPX の生成が使う推定) の回帰の見張り。`estimateArrivalTimes` と +//! `connectedRoutes` は元の較正のままなので、ここでは測らない。 //! //! 基準ごとに推定の所要時間を出し、実際の典型的な所要時間 (平日日中の中央値) から //! のずれを求める。記録した推定 (`travel_times/baseline.csv`) より悪くなった基準が @@ -21,7 +23,7 @@ use std::collections::HashMap; use stationapi::domain::repository::station_repository::StationRepository; -use stationapi::model::RouteLegRequest; +use stationapi::model::{RouteLegRequest, TrainRouteModel}; use stationapi::use_case::traits::query::QueryUseCase; use crate::repository::MemStationRepository; @@ -154,17 +156,22 @@ fn estimate(case: &Case) -> Option { from_station_id: case.from_station_id, to_station_id: case.slice_end_station_id, }]; - let stops = block_on(crate::interactor().estimate_connected_route_arrival_times(&legs)) - .unwrap_or_else(|e| panic!("{}: 推定できない: {e}", case.label)); - let stop = stops + // 見張るのは trainRoute の Estimated (GPX の生成が使う推定)。estimateArrivalTimes と + // connectedRoutes は元の較正のままで、推定の規則や較正を変えても動かない + let segments = + block_on(crate::interactor().get_connected_train_route(&legs, TrainRouteModel::Estimated)) + .unwrap_or_else(|e| panic!("{}: 推定できない: {e}", case.label)); + let segment = segments .iter() .skip(1) - .find(|stop| stop.station_cd as u32 == case.to_station_id) + .find(|segment| { + segment.station.as_ref().map(|station| station.id) == Some(case.to_station_id) + }) .unwrap_or_else(|| panic!("{}: 推定の駅列に到着駅が無い", case.label)); - Some(match case.measure { - Measure::Arrival => stop.cumulative_minutes, - Measure::Departure => stop.departure_cumulative_minutes, - }) + match case.measure { + Measure::Arrival => segment.arrival_cumulative_minutes, + Measure::Departure => segment.departure_cumulative_minutes, + } } fn read_baseline() -> HashMap { diff --git a/stationapi/src/domain/arrival_estimation.rs b/stationapi/src/domain/arrival_estimation.rs index 087f1905..1f732f60 100644 --- a/stationapi/src/domain/arrival_estimation.rs +++ b/stationapi/src/domain/arrival_estimation.rs @@ -42,6 +42,9 @@ use std::collections::HashMap; use crate::domain::entity::gtfs::TransportType; use crate::domain::entity::station::Station; +use crate::domain::legacy_speed_table::{ + legacy_line_speed_override_kmh, legacy_segment_speed_override_kmh, +}; use crate::domain::segment_speed_table::{ segment_override_applies_to_kind, segment_speed_override_kmh, }; @@ -72,6 +75,19 @@ pub struct EstimatedStop { pub max_speed_kmh: f64, } +/// 速度の較正にどの表を使うか。 +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum SpeedCalibration { + /// 元の較正 (`legacy_speed_table`。#1712 で求め直す前の表)。`estimateArrivalTimes`、 + /// `connectedRoutes` など、`trainRoute` の `Estimated` 以外はすべてこれを使う。 + #[default] + Original, + /// 駅間の距離に線路の長さを使う前提で求め直した較正 (`speed_table` / + /// `segment_speed_table`)。`trainRoute` の `Estimated` (MobileApp の GPX の生成) + /// だけが使う。 + Recalibrated, +} + /// 推定で使う調整可能なパラメータ。すべて「実距離・実速度・ダイヤが無い」前提の /// ヒューリスティックであり、後から較正・上書きできるよう一箇所に集約する。 #[derive(Clone, Copy, Debug)] @@ -93,6 +109,8 @@ pub struct EstimationParams { pub detour_min: f64, /// 迂回係数 `α` のクランプ上限。 pub detour_max: f64, + /// 速度の較正にどの表を使うか。 + pub speed_calibration: SpeedCalibration, } impl Default for EstimationParams { @@ -105,6 +123,7 @@ impl Default for EstimationParams { pass_penalty_seconds: 3.0, detour_min: 1.0, detour_max: 1.6, + speed_calibration: SpeedCalibration::Original, } } } @@ -367,11 +386,16 @@ fn max_speed_kmh( line_type: Option, kind: Option, transport_type: TransportType, + calibration: SpeedCalibration, ) -> f64 { if transport_type == TransportType::Bus { return BUS_MAX_SPEED_KMH; } - if let Some(v) = line_speed_override_kmh(line_cd, kind) { + let calibrated = match calibration { + SpeedCalibration::Original => legacy_line_speed_override_kmh(line_cd, kind), + SpeedCalibration::Recalibrated => line_speed_override_kmh(line_cd, kind), + }; + if let Some(v) = calibrated { return v; } let base = base_speed_kmh(line_type); @@ -658,19 +682,26 @@ pub fn estimate_arrival_minutes_with_track( stops[i].line_type, effective_kind, stops[i].transport_type, + params.speed_calibration, ); // 隣接駅ペア単位の較正(GTFS 実ダイヤ由来)があれば路線単位の速度より // 優先する。急曲線・急勾配で路線平均より遅い区間(大江戸線 月島〜赤羽橋 // など)の区間差を反映する。各停系種別の鉄道のみ。 - if i > 0 - && stops[i].transport_type != TransportType::Bus - && segment_override_applies_to_kind(effective_kind) - { - if let Some(v) = segment_speed_override_kmh( + if i > 0 && stops[i].transport_type != TransportType::Bus { + let (line_cd, a, b) = ( stops[i].line_cd, stops[i - 1].station_cd, stops[i].station_cd, - ) { + ); + let calibrated = match params.speed_calibration { + SpeedCalibration::Original => { + legacy_segment_speed_override_kmh(line_cd, a, b, effective_kind) + } + SpeedCalibration::Recalibrated => segment_override_applies_to_kind(effective_kind) + .then(|| segment_speed_override_kmh(line_cd, a, b)) + .flatten(), + }; + if let Some(v) = calibrated { v_kmh = v; } } diff --git a/stationapi/src/domain/legacy_speed_table.rs b/stationapi/src/domain/legacy_speed_table.rs index c3e395c0..9a1a5221 100644 --- a/stationapi/src/domain/legacy_speed_table.rs +++ b/stationapi/src/domain/legacy_speed_table.rs @@ -1,10 +1,13 @@ -//! `trainRoute` の `Legacy` モデル (MobileApp のオートモード) が使う速度の較正テーブル。 +//! 元の速度の較正テーブル。`trainRoute` の `Estimated` 以外のすべて +//! (`estimateArrivalTimes`、`connectedRoutes`、`trainRoute` の `Legacy` = MobileApp の +//! オートモード) が使う。 //! -//! 到着時間推定の較正 (`speed_table` / `segment_speed_table`) は、距離の出し方を -//! 線路の長さへ替えたときに求め直した。`Legacy` はオートモードの走り方を変えない -//! ために、求め直す前の表 (TrainLCD/StationAPI の `dev`、#1711 の時点) をここに -//! 凍結して持つ。`scripts/compute_speed_table.py` はこのファイルを書き換えない。 -//! 値を変えるとオートモードの速度が変わるので、変えないこと。 +//! `speed_table` / `segment_speed_table` は、駅間の距離に線路の長さを使う前提で +//! 求め直した (#1712)。求め直した較正は `trainRoute` の `Estimated` (MobileApp の GPX の +//! 生成) だけが使い、ほかの推定の値が変わらないよう、求め直す前の表 +//! (TrainLCD/StationAPI の `dev`、#1711 の時点) をここに凍結して持つ。 +//! `scripts/compute_speed_table.py` はこのファイルを書き換えない。値を変えると +//! ETA・経路検索・オートモードが変わるので、変えないこと。 use crate::domain::segment_speed_table::segment_override_applies_to_kind; use crate::model::TrainTypeKind; diff --git a/stationapi/src/domain/segment_speed_table.rs b/stationapi/src/domain/segment_speed_table.rs index b917e993..a29765b4 100644 --- a/stationapi/src/domain/segment_speed_table.rs +++ b/stationapi/src/domain/segment_speed_table.rs @@ -6,6 +6,9 @@ //! この残差を原理的に解消できないため、公開 GTFS 時刻表から隣接駅ペア単位で //! フィットした実効速度をここで上書きする。 //! +//! `trainRoute` の `Estimated` の推定 (`SpeedCalibration::Recalibrated`) だけが +//! 参照する。ほかの推定は、求め直す前の表 (`legacy_speed_table`) を使う。 +//! //! 生成は `scripts/compute_speed_table.py --apply`(路線単位テーブルと同時生成)。 //! 駅間の純走行時間(次駅の到着時刻が別記録されていれば「出発 → 次駅到着」、 //! 無ければ「出発 → 次駅出発 − モデル停車時分」)を複数本の平均でデクオンタイズし、 diff --git a/stationapi/src/domain/speed_table.rs b/stationapi/src/domain/speed_table.rs index 0bc23528..a357dfe2 100644 --- a/stationapi/src/domain/speed_table.rs +++ b/stationapi/src/domain/speed_table.rs @@ -9,8 +9,9 @@ //! //! 値の意味は「その路線・種別での実効巡航速度(km/h)」。理論上の車両性能では //! なく、時刻表所要時間を運動学モデルで再現する値として較正している。 -//! 到着時間推定と GetTrainRoute の速度プロファイル(`resolve_speed_profile`)の -//! 両方から参照される。 +//! `trainRoute` の `Estimated` の推定 (`SpeedCalibration::Recalibrated`) だけが +//! 参照する。`estimateArrivalTimes`・`connectedRoutes`・`trainRoute` の `Legacy` は、 +//! 求め直す前の表 (`legacy_speed_table`) を使う。 //! //! エントリ追加の指針: //! - 公表運転速度(例: 京急快特 120km/h、スカイライナー 160km/h)を起点にし、 @@ -32,8 +33,8 @@ use crate::model::TrainTypeKind; /// 距離が直線 × 迂回係数だった頃の値は、距離の水増しを速度で打ち消していたので、 /// 線路の長さへ替えたときに求め直した。小田急線 (快速急行) は一般則で典型値に /// 近づいたので外した。つくばエクスプレスと都営大江戸線は GTFS の自動較正に任せる。 -/// オートモード (`trainRoute` の `Legacy`) は求め直す前の値を `legacy_speed_table` -/// で使い続ける。 +/// `estimateArrivalTimes`・`connectedRoutes`・`trainRoute` の `Legacy` は、求め直す前の +/// 値を `legacy_speed_table` で使い続ける。 const LINE_SPEED_OVERRIDES: &[(i32, TrainTypeKind, f64)] = &[ // 総武快速線: 最高 130km/h の別線を走る。StationAPI の路線には快速の停車駅しか // 無く、通過駅が無いので推定は各停 (Default) として扱う。一般則の 80km/h では diff --git a/stationapi/src/use_case/interactor/query.rs b/stationapi/src/use_case/interactor/query.rs index b745517c..4ac221e9 100644 --- a/stationapi/src/use_case/interactor/query.rs +++ b/stationapi/src/use_case/interactor/query.rs @@ -25,7 +25,7 @@ use crate::{ domain::{ arrival_estimation::{ estimate_arrival_minutes_with_track, is_circular_route, select_circular_arc, - track_distance_key, EstimatedStop, EstimationParams, TrackDistances, + track_distance_key, EstimatedStop, EstimationParams, SpeedCalibration, TrackDistances, }, entity::{ company::Company, @@ -947,60 +947,7 @@ where &self, legs: &[model::RouteLegRequest], ) -> Result, UseCaseError> { - let group_of = self.validate_route_legs(legs).await?; - let params = EstimationParams::default(); - let walk_minutes = f64::from(route_search::TRANSFER_WALK_SECONDS) / 60.0; - - let mut result: Vec = Vec::new(); - for (index, leg) in legs.iter().enumerate() { - // trainRoute と同じく系統の駅列から切り出す。系統に無い乗降駅は駅グループで - // 引き当てるので、区間の trainTypes のどの種別を lineGroupId にしてもよい - let stations = self - .station_repository - .get_by_line_group_id(leg.line_group_id) - .await?; - let group_stops: Vec<&Station> = stations.iter().collect(); - let track = self.track_distances_of(&group_stops).await?; - let (from_group, to_group) = - (group_of[&leg.from_station_id], group_of[&leg.to_station_id]); - let segment = estimate_group_segment( - &group_stops, - SegmentEndpoints { - from_station_cd: leg.from_station_id, - to_station_cd: leg.to_station_id, - groups: Some((from_group, to_group)), - }, - false, - ¶ms, - Some(&track), - ) - .ok_or_else(|| leg_not_found(leg))?; - - // 乗換では、乗換駅に歩いて着いた時刻を到着、乗換先の列車を待った後を - // 出発とする。見込みは connectedRoutes の所要時間と同じ (最初の列車の - // 待ち時間は含めない) - let (board_arrival, board_departure) = match result.last() { - Some(previous) if index > 0 => { - let wait_minutes = f64::from(route_search::boarding_wait_seconds( - group_stops.first().and_then(|s| s.kind), - )) / 60.0; - let arrival = previous.cumulative_minutes + walk_minutes; - (arrival, arrival + wait_minutes) - } - _ => (0.0, 0.0), - }; - for (position, mut stop) in segment.into_iter().enumerate() { - if position == 0 { - stop.cumulative_minutes = board_arrival; - stop.departure_cumulative_minutes = board_departure; - } else { - stop.cumulative_minutes += board_departure; - stop.departure_cumulative_minutes += board_departure; - } - result.push(stop); - } - } - Ok(result) + self.estimate_legs(legs, false).await } async fn get_connected_train_route( @@ -1035,10 +982,11 @@ where // 区間ごとに別の列車なので、通過駅の有無や距離の起点も区間ごとに数える segments.extend(self.train_route_segments(sliced, leg.line_group_id).await?); } - // 見込みは estimateArrivalTimes に同じ legs を渡したときと同じ値にする - // (乗換の徒歩と待ち時間の見込みも含む)。バスは推定のモデルの対象外 + // 見込みは estimateArrivalTimes に同じ legs を渡したときと同じ組み立て方 + // (乗換の徒歩と待ち時間の見込みも含む) で、較正と駅間の距離だけを Estimated の + // もの (求め直した較正と線路の長さ) にする。バスは推定のモデルの対象外 if estimated_model && !has_bus { - let estimated = self.estimate_connected_route_arrival_times(legs).await?; + let estimated = self.estimate_legs(legs, true).await?; apply_estimated_model(&mut segments, &estimated)?; } Ok(segments) @@ -1189,13 +1137,12 @@ where let mut result: Vec = Vec::new(); for group_stops in route_row_tree_map.values() { - let track = self.track_distances_of(group_stops).await?; if let Some(segment) = estimate_group_segment( group_stops, SegmentEndpoints::exact(from_station_id, to_station_id), direction_id.is_some(), ¶ms, - Some(&track), + None, ) { result.extend(segment); } @@ -1212,6 +1159,79 @@ where TR: TrainTypeRepository, CR: CompanyRepository, { + /// 区間 (`legs`) をつないだ経路の到着見込み。`estimated_model` が真なら + /// `trainRoute` の `Estimated` 用で、求め直した較正と線路の長さで推定する。 + /// 偽なら `estimateArrivalTimes` 用で、元の較正と直線距離 × 迂回係数で推定する + /// (`connectedRoutes` の所要時間と同じ見積もり)。 + async fn estimate_legs( + &self, + legs: &[model::RouteLegRequest], + estimated_model: bool, + ) -> Result, UseCaseError> { + let group_of = self.validate_route_legs(legs).await?; + let params = if estimated_model { + estimated_model_params() + } else { + EstimationParams::default() + }; + let walk_minutes = f64::from(route_search::TRANSFER_WALK_SECONDS) / 60.0; + + let mut result: Vec = Vec::new(); + for (index, leg) in legs.iter().enumerate() { + // trainRoute と同じく系統の駅列から切り出す。系統に無い乗降駅は駅グループで + // 引き当てるので、区間の trainTypes のどの種別を lineGroupId にしてもよい + let stations = self + .station_repository + .get_by_line_group_id(leg.line_group_id) + .await?; + let group_stops: Vec<&Station> = stations.iter().collect(); + let track = if estimated_model { + Some(self.track_distances_of(&group_stops).await?) + } else { + None + }; + let (from_group, to_group) = + (group_of[&leg.from_station_id], group_of[&leg.to_station_id]); + let segment = estimate_group_segment( + &group_stops, + SegmentEndpoints { + from_station_cd: leg.from_station_id, + to_station_cd: leg.to_station_id, + groups: Some((from_group, to_group)), + }, + false, + ¶ms, + track.as_ref(), + ) + .ok_or_else(|| leg_not_found(leg))?; + + // 乗換では、乗換駅に歩いて着いた時刻を到着、乗換先の列車を待った後を + // 出発とする。見込みは connectedRoutes の所要時間と同じ (最初の列車の + // 待ち時間は含めない) + let (board_arrival, board_departure) = match result.last() { + Some(previous) if index > 0 => { + let wait_minutes = f64::from(route_search::boarding_wait_seconds( + group_stops.first().and_then(|s| s.kind), + )) / 60.0; + let arrival = previous.cumulative_minutes + walk_minutes; + (arrival, arrival + wait_minutes) + } + _ => (0.0, 0.0), + }; + for (position, mut stop) in segment.into_iter().enumerate() { + if position == 0 { + stop.cumulative_minutes = board_arrival; + stop.departure_cumulative_minutes = board_departure; + } else { + stop.cumulative_minutes += board_departure; + stop.departure_cumulative_minutes += board_departure; + } + result.push(stop); + } + } + Ok(result) + } + /// 乗換経路の区間の並びが 1 本の経路としてつながっているか確かめる。 /// 前の区間の降車駅と次の区間の乗車駅は同じ駅グループでなければならない。 /// 返り値は、区間の乗降駅の `station_cd` -> 駅グループ。 @@ -2085,11 +2105,21 @@ fn estimate_train_route_slice( Some(estimate_arrival_minutes_with_track( &stops, &route, - &EstimationParams::default(), + &estimated_model_params(), Some(track), )) } +/// `trainRoute` の `Estimated` だけが使う推定のパラメータ。駅間の距離に線路の長さを +/// 使う前提で求め直した較正を使う。ほかの推定 (`estimateArrivalTimes`、 +/// `connectedRoutes`) は元の較正 ([`EstimationParams::default`]) のまま。 +fn estimated_model_params() -> EstimationParams { + EstimationParams { + speed_calibration: SpeedCalibration::Recalibrated, + ..EstimationParams::default() + } +} + /// `trainRoute` の区間の値を、到着見込み `estimated` のモデルの値に置き換える /// (`TrainRouteModel::Estimated`)。 /// @@ -3831,12 +3861,9 @@ mod tests { .push(station.clone()); } } - // get_track_distances と同じ長さで組み立て、見込みと探索の所要時間をそろえる - let distance = self.track_distance; - Ok(std::sync::Arc::new(RouteNetwork::build_with_track( + Ok(std::sync::Arc::new(RouteNetwork::build( by_line_group.into_values(), &EstimationParams::default(), - move |_, _| distance, ))) } async fn get_track_distances( @@ -6462,8 +6489,8 @@ mod tests { .all(|s| s.max_acceleration == 0.83 && s.max_deceleration == 0.69)); } - /// Estimated は、同じ駅列を系統全体で較正し、系統の線路の長さを渡した - /// 到着時間推定の値を返す。 + /// Estimated は、同じ駅列を系統全体で較正し、系統の線路の長さと求め直した + /// 較正 (`SpeedCalibration::Recalibrated`) を渡した到着時間推定の値を返す。 #[tokio::test] async fn estimated_model_follows_the_arrival_estimation() { let group = build_line_group(20); @@ -6475,7 +6502,7 @@ mod tests { let slice: Vec<&Station> = group[2..=6].iter().collect(); let whole: Vec<&Station> = group.iter().collect(); - let params = EstimationParams::default(); + let params = estimated_model_params(); let track = interactor.track_distances_of(&whole).await.unwrap(); assert!( !track.is_empty(), diff --git a/travel_times/README.md b/travel_times/README.md index f902f420..0b214027 100644 --- a/travel_times/README.md +++ b/travel_times/README.md @@ -1,7 +1,8 @@ # travel_times/ -到着時間推定 (`stationapi/src/domain/arrival_estimation.rs`) の所要時間を、実際の -列車の所要時間と比べるための基準を置く場所です。速度の較正テーブルや一般則は、 +`trainRoute` の `Estimated` (MobileApp の GPX の生成が使う到着時間推定) の所要時間を、 +実際の列車の所要時間と比べるための基準を置く場所です。`estimateArrivalTimes` と +`connectedRoutes` は元の較正のままなので、ここでは測りません。速度の較正テーブルや一般則は、 1 つの路線に合わせて変えると、同じ規則を使うほかの路線の推定も変わります。 変更の前後で全体の誤差を測り、局所的な合わせ込みで全体が崩れないようにします。 @@ -50,7 +51,7 @@ も表に出しますが、判定には使いません。記録にある基準を 推定できなくなったとき (種別グループがデータから消えたときなど) も失敗します。 -比べるのは、本番と同じ生成データ (`generated/`) で動くときだけです。到着時間推定は、 +比べるのは、本番と同じ生成データ (`generated/`) で動くときだけです。`Estimated` は、 生成データにしか無い線路の長さや種別グループを使うので、`data/*.csv` だけでは本番の 推定を再現できません。CI では `build_worker.yml` が `generated/` を作ってから走らせ ます。`data/*.csv` で動くとき (`ci.yml` のテストなど) は、表を出すだけにします。 @@ -65,7 +66,7 @@ TRAVEL_TIMES_UPDATE_BASELINE=1 cargo test -p stationapi-worker travel_times ### レポート (本番と同じデータでの精度) -動いている Worker に `estimateArrivalTimes` を問い合わせ、全件の誤差を Markdown で +動いている Worker に `trainRoute` (`model: Estimated`) を問い合わせ、全件の誤差を Markdown で 出します。`make data && make dev` で起動した Worker (既定) か、ステージングに 向けます。 From 3dd6ab3259a05b662af07217512eea79a645dcfe Mon Sep 17 00:00:00 2001 From: Tsubasa SEKIGUCHI Date: Thu, 1 Oct 2026 10:25:49 +0900 Subject: [PATCH 5/5] =?UTF-8?q?estimateArrivalTimes=E3=82=92trainRoute?= =?UTF-8?q?=E3=81=AEEstimated=E3=81=A8=E5=90=8C=E3=81=98=E6=B1=82=E3=82=81?= =?UTF-8?q?=E7=9B=B4=E3=81=97=E3=81=9F=E8=BC=83=E6=AD=A3=E3=81=A8=E7=B7=9A?= =?UTF-8?q?=E8=B7=AF=E3=81=AE=E9=95=B7=E3=81=95=E3=81=A7=E6=8E=A8=E5=AE=9A?= =?UTF-8?q?=E3=81=97=E3=80=81connectedRoutes=E3=81=AF=E5=85=83=E3=81=AE?= =?UTF-8?q?=E8=A8=88=E7=AE=97=E3=81=AE=E3=81=BE=E3=81=BE=E6=AE=8B=E3=81=97?= =?UTF-8?q?=E3=81=9F=20(#1715)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 6 +-- docs/architecture.md | 26 ++++----- scripts/README.md | 6 +-- scripts/travel_time_report.py | 4 +- src/graphql/enums.rs | 3 +- src/repository.rs | 56 +++++++++----------- src/travel_times.rs | 10 ++-- stationapi/src/domain/arrival_estimation.rs | 8 +-- stationapi/src/domain/legacy_speed_table.rs | 11 ++-- stationapi/src/domain/route_search.rs | 5 +- stationapi/src/domain/segment_speed_table.rs | 5 +- stationapi/src/domain/speed_table.rs | 10 ++-- stationapi/src/model.rs | 2 +- stationapi/src/use_case/interactor/query.rs | 50 +++++++---------- travel_times/README.md | 6 +-- 15 files changed, 97 insertions(+), 111 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index fee7d1de..b2dbf5f7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -71,16 +71,16 @@ The Worker is the workspace root package. `stationapi`, `preprocessor`, and `dat - **Stations** – `station`, `stations`, `stationGroupStations`, `stationsNearby`, `lineStations`, `stationsByName`, `lineGroupStations`, `lineListStations`, `lineGroupListStations`. `QueryInteractor` enriches stations with lines, companies, station numbers, and train types. `lineStations` resolves the line's local train-type group (rail `kind` 0/1 or a `priority > 0` type; a bullet-train line, which has no local service, takes the group of its stopping train type with the smallest `types.id`, e.g. Nozomi or Hayabusa) and returns its stops carrying that group's train type, with or without `stationId`, so a client that never picked a train type still has the `lineGroupId` `trainRoute` requires; when no such group exists — bus lines only carry `BusRoute` (`kind` 7, `priority` 0) variants — it falls back to the line's plain typeless station list so bus stop listings never return empty. `stationsByName` with `fromStationGroupId` returns the stations reachable from there: stations sharing a line group with the origin (`line_group_cd` set, `has_train_types` true), same-line stations when either side has no line group, and — for rail — stations reachable by transferring, i.e. those for which `connectedRoutes` with `viaLineId` set to the station's line returns a route (`line_group_cd` empty, `has_train_types` false). The transfer check uses `RouteTopology` (`stationapi/src/domain/route_topology.rs`), a time-free copy of the `connectedRoutes` network built straight from the index without `Station` entities or time estimates (about 20 ms instead of about 190 ms, cached in its own `OnceLock`); `RouteNetwork` holds the same topology, both share `trim_pattern` and `line_group_rows`, and a real-data test asserts the two are equal. The check is a ride-limited BFS over line groups (a few ms) plus, only for destinations that are cut vertices of the station–line-group graph, a check that arrives without stopping over at the destination group — otherwise a branch's junction station (Ishibashi-handai-mae on the Minoo Line) would be listed although reaching it on that branch means riding out and back. - **Lines** – `line`, `lines`, `linesByName`. Results include company data and computed line symbols based on repository helpers. - **Routes** – `routes`, `connectedRoutes`, `estimateArrivalTimes`, `trainRoute`. Paging tokens are currently empty (pagination not implemented). -- **`trainRoute`** – Takes the line group's stops from the repository *before* any enrichment, slices them to the requested `fromStationId`–`toStationId` range (reversing when the request runs backwards), and only then attaches lines, companies, station numbers, train types, and nearby bus routes. Enrichment is per-station and independent, so slicing first does not change any segment; enriching the whole line group first made a three-station request cost the same as a 250-station one. Keep the order — the cost of this query must stay proportional to the requested range, not to the line group. The optional `model` argument (`TrainRouteModel`) picks how the segment values are computed. `Legacy`, the default, keeps the values from when the query was added (#1568): `dto::simulation::resolve_speed_profile` supplies the speed and acceleration, and `arrivalCumulativeMinutes` / `departureCumulativeMinutes` stay `null`. TrainLCD/MobileApp's auto mode depends on these values, so do not change them; `Legacy` reads frozen copies of the speed tables (`domain/legacy_speed_table.rs`) so that recalibrating `Estimated` does not move it. `Estimated` runs `arrival_estimation` — the model the speed tables are calibrated against — on the same pre-enrichment slice (with `legs`, it reuses `estimate_connected_route_arrival_times`, so the values equal `estimateArrivalTimes` given the same `legs`). It then replaces each segment's `stops`, speed, and acceleration with the estimator's and fills the two cumulative fields. A route containing a bus station keeps the `Legacy` values, because the estimator's kinematics do not apply to buses. +- **`trainRoute`** – Takes the line group's stops from the repository *before* any enrichment, slices them to the requested `fromStationId`–`toStationId` range (reversing when the request runs backwards), and only then attaches lines, companies, station numbers, train types, and nearby bus routes. Enrichment is per-station and independent, so slicing first does not change any segment; enriching the whole line group first made a three-station request cost the same as a 250-station one. Keep the order — the cost of this query must stay proportional to the requested range, not to the line group. The optional `model` argument (`TrainRouteModel`) picks how the segment values are computed. `Legacy`, the default, keeps the values from when the query was added (#1568): `dto::simulation::resolve_speed_profile` supplies the speed and acceleration, and `arrivalCumulativeMinutes` / `departureCumulativeMinutes` stay `null`. Older TrainLCD/MobileApp builds run auto mode on these values, so do not change them; `Legacy` reads frozen copies of the speed tables (`domain/legacy_speed_table.rs`) so that recalibrating the estimation does not move it. `Estimated` (TrainLCD/MobileApp's auto mode and GPX generation, so that auto mode, ETA, and GPX take the same time) runs `arrival_estimation` — the model the speed tables are calibrated against — on the same pre-enrichment slice with the same calibration and distances as `estimateArrivalTimes` (with `legs`, it reuses `estimate_connected_route_arrival_times`, so the values equal `estimateArrivalTimes` given the same `legs`). It then replaces each segment's `stops`, speed, and acceleration with the estimator's and fills the two cumulative fields. A route containing a bus station keeps the `Legacy` values, because the estimator's kinematics do not apply to buses. - **Coordinate lookups** – `index::nearest` (k nearest, used by `stationsNearby`) and `index::within_radius` (everything inside a radius, used by the nearby-bus-stop enrichment) both go through a per-transport-type grid index (`Grid`, CSR over 0.05° cells) instead of scanning the whole station table. `nearest` searches a radius, widens it while fewer than `limit` stations fall inside, and stops once the radius covers the index — anything outside a radius that already holds `limit` hits cannot be in the top `limit`. With `transportType` omitted it returns rail stations first and bus stops after, each group sorted by distance — the pre-Workers SQL's `ORDER BY transport_type, distance`. The limit applies to the merged order, so `nearest` fills it with rail and only asks the bus grid for the remaining slots; a location with `limit` rail stations returns no bus stops at all. Ties on distance break on `station_cd` so the order does not depend on an unstable sort. Every station lookup by coordinates runs on every request that enriches rail stations with nearby bus routes, so keep new coordinate queries on the grid rather than adding another full scan. - **Train types** – `stationTrainTypes`, `routeTypes`. Train types aggregate by line group and include related lines plus optional train type metadata. Rail variants use `TrainTypeKind::{Default, Branch, Rapid, Express, LimitedExpress, HighSpeedRapid, CommuterRapid}` (0-6); bus variants use `BusRoute` (7), which represents a `(route_id, shape_id)` operation pattern (e.g. 循環 / 短ターン / 支線) generated automatically from the configured GTFS bus feeds (Toei Bus, Seibu Bus, Keio Bus) and the converted Tokyu Bus JSON. - **Default rail train types** – `preprocessor` fills every active rail line containing at least one station with no `station_station_types` row with a deterministic, complete all-stop group. The generated rows exist only in `generated/*.csv`; canonical CSV files remain unchanged. `type_cd=100` represents 「普通」 and `type_cd=101` represents 「各駅停車」. An existing 100/101 assignment on the line takes precedence; otherwise the label is selected per line through `LOCAL_SERVICE_RAIL_LINE_IDS` in `preprocessor/src/rail.rs`. Generated `line_group_cd` values use `1,000,000,000 + line_cd`; generation fails on a collision. Bus lines are excluded and continue to use their GTFS-derived `BusRoute` groups. - **GTFS bus integration** – `preprocessor/src/gtfs/` reads the GTFS feeds into an in-memory representation and then projects them onto the shared `stations` / `lines` / `types` / `station_station_types` tables (`gtfs/integrate.rs`). Only routes, stops, trips, and stop_times are read; calendar, shapes, feed_info, and agencies do not affect the output. Every configured GTFS feed is imported, including Seibu Bus and Keio Bus (both downloaded from ODPT with `ODPT_ACCESS_TOKEN`). Tokyu Bus ordinary-route `BusroutePattern`, `BusstopPole`, and `BusTimetable` JSON are converted into the same representation; pattern IDs become `shape_id` values so route variants remain queryable as bus TrainTypes. The Tokyu-operated Ota, Shinagawa, and Meguro community buses use their official GTFS feeds and matching JSON routes are excluded to prevent duplicates. `ODPT_ACCESS_TOKEN` is required for authenticated sources; without it those feeds are skipped with a warning rather than failing the build. Stops whose Tokyu JSON records omit coordinates remain available to name and route queries but not coordinate searches. `transport_type` (0: rail, 1: bus) on both `stations` and `lines` keeps rail and bus records queryable side by side. GTFS IDs are namespaced per feed before import to avoid cross-operator collisions. `line_cd` (100,000,000+), `station_cd` / `station_g_cd` (200,000,000+), and bus `type_cd` / `line_group_cd` (100,000,000+) are all deterministic fnv1a hashes that stay clear of the rail data ranges. Disable the entire bus pipeline with `DISABLE_BUS_FEATURE=true`. - **Bus stop translations (readings & English)** – GTFS-JP `translations.txt` layouts differ per feed, so `load_translations` (`preprocessor/src/gtfs/parse.rs`) resolves columns by header name (Seibu ships 6 columns without `record_sub_id`; Keio and the Tokyu community feeds ship 7) and indexes each `stop_name` translation under both keys it may use: `record_id` (== the stop_id, Seibu — with the "-NN" pole suffix also mapped to the parent stop_id) and `field_value` (== the Japanese stop_name, Keio / Tokyu community, where `record_id` is left empty). `load_stops` then looks a stop's translation up by stop_id first, then by name. Keying only by `record_id` would silently drop every field_value-keyed feed, leaving `station_name_k` filled with the kanji stop_name and `station_name_r` empty. Readings arriving as half-width katakana (`ニシハチオウジ`, Keio / Tokyu community) are folded to full-width via `romaji::to_fullwidth_katakana()` before storage. - **Bus English-name fallback** – When a feed provides no English (`en`) translation for a stop — e.g. Tokyu Bus ordinary-route JSON, which carries only `dc:title` and `odpt:kana` — `stationapi/src/domain/romaji.rs::romaji_display_name()` derives a modified-Hepburn romanization (with macrons for long vowels, matching the curated rail style: Tōkyō / Kyōto / Shin-Ōsaka) from the kana reading, and the GTFS reader fills `stop_name_r` with it. The fallback never overwrites a real `en` value, and a reading with no convertible kana stays `NULL` rather than emitting a partial transcription. Because `stop_name_r` is the single upstream source that fans out into the `stations` projection, `search_by_name`, and the romanized bus route/headsign names, this supplements every English-facing surface at once. When projecting into `stations`, `station_name_rn` is filled with the plain-ASCII spelling via `romaji::strip_macrons()` (Tōkyō → Tokyo), mirroring the rail dataset's `_r` (macron) / `_rn` (macron-free) column pair. -- **Track distances** – `Station.trackDistanceFromPrevious` is the track length in meters from the station before it in the returned list, for clients that total a ride's distance (TrainLCD/MobileApp's ride log). It is distinct from `trainRoute`'s `distanceFromPrevious`, which stays the straight-line distance the app's running simulation relies on. Only `trainRoute` with `Estimated` (used by TrainLCD/MobileApp's GPX generation) uses the track length as the running distance (straight line × detour factor where none exists) together with the recalibrated speed tables (`speed_table` / `segment_speed_table`, `SpeedCalibration::Recalibrated`); `scripts/compute_speed_table.py` fits those tables with the same distances, so it needs `generated/` from `make data`. Everything else — `estimateArrivalTimes`, the ride times of `connectedRoutes`, and `trainRoute` with `Legacy` — keeps the original estimation (straight line × detour factor and the frozen tables in `domain/legacy_speed_table.rs`, the `EstimationParams` default), so recalibrating never moves ETAs, route search results, or auto mode. Only `lineStations`, `lineGroupStations`, `trainRoute`, and `stations(ids)` fill it (`attach_track_distances` in `QueryInteractor`, called once the order is final). `stations(ids)` returns the stations in the order of `ids` — the repository keeps that order and enrichment never reorders — so a client passing a route's station IDs (MobileApp's `sids` deep links) gets the lengths between IDs adjacent in that order; a pair that is not adjacent in the track data (an ID list skipping stations) is `null`. The first station, the first station of each `trainRoute` leg, sections without N02 geometry, and every other query return `null`, and clients fall back to the straight line there. `preprocessor/src/track/` builds an undirected graph from N02's `RailroadSection` LineStrings, collects every pair adjacent in a line's `(e_sort, station_cd)` order or a line group's `station_station_types.id` order (with and without closed stations, plus the seam of loop services within 3 km), snaps each station to every track within 500 m, and takes the Dijkstra path minimizing `2 × snap offset + track length` — snapping to the nearest track alone picks another line of the same operator at large stations (Honmachi) and detours through a transfer station. It first measures on the operators of both stations' companies (`OPERATOR_ALIASES` maps companies whose name differs from N02's), then retries on every operator for lines running on another company's track; a path longer than max(3 × straight, straight + 5 km) counts as unmeasured, and a result shorter than the straight line is raised to it. Pairs in one station group are 0. `build.rs` writes the sorted pairs to `connections.bin`, and `index::track_distance` binary-searches it, so no index is built at isolate start. When bumping the N02 edition, change the URL and cache directory in `preprocessor/src/track/mod.rs` and the `Cache N02 railway data` step in `.github/actions/build-worker/action.yml` together and compare the unmeasured count in the `preprocessor` log. `docs/architecture.md` (駅間の線路の長さ) has the details. +- **Track distances** – `Station.trackDistanceFromPrevious` is the track length in meters from the station before it in the returned list, for clients that total a ride's distance (TrainLCD/MobileApp's ride log). It is distinct from `trainRoute`'s `distanceFromPrevious`, which stays the straight-line distance the app's running simulation relies on. The arrival estimation (`estimateArrivalTimes` and `trainRoute` with `Estimated`) uses the track length as the running distance (straight line × detour factor where none exists) together with the recalibrated speed tables (`speed_table` / `segment_speed_table`, `SpeedCalibration::Recalibrated`); `scripts/compute_speed_table.py` fits those tables with the same distances, so it needs `generated/` from `make data`. The ride times of `connectedRoutes` and `trainRoute` with `Legacy` keep the original estimation (straight line × detour factor and the frozen tables in `domain/legacy_speed_table.rs`, the `EstimationParams` default), so recalibrating never moves route search results. Route search ride times therefore differ from the ETA. Only `lineStations`, `lineGroupStations`, `trainRoute`, and `stations(ids)` fill it (`attach_track_distances` in `QueryInteractor`, called once the order is final). `stations(ids)` returns the stations in the order of `ids` — the repository keeps that order and enrichment never reorders — so a client passing a route's station IDs (MobileApp's `sids` deep links) gets the lengths between IDs adjacent in that order; a pair that is not adjacent in the track data (an ID list skipping stations) is `null`. The first station, the first station of each `trainRoute` leg, sections without N02 geometry, and every other query return `null`, and clients fall back to the straight line there. `preprocessor/src/track/` builds an undirected graph from N02's `RailroadSection` LineStrings, collects every pair adjacent in a line's `(e_sort, station_cd)` order or a line group's `station_station_types.id` order (with and without closed stations, plus the seam of loop services within 3 km), snaps each station to every track within 500 m, and takes the Dijkstra path minimizing `2 × snap offset + track length` — snapping to the nearest track alone picks another line of the same operator at large stations (Honmachi) and detours through a transfer station. It first measures on the operators of both stations' companies (`OPERATOR_ALIASES` maps companies whose name differs from N02's), then retries on every operator for lines running on another company's track; a path longer than max(3 × straight, straight + 5 km) counts as unmeasured, and a result shorter than the straight line is raised to it. Pairs in one station group are 0. `build.rs` writes the sorted pairs to `connections.bin`, and `index::track_distance` binary-searches it, so no index is built at isolate start. When bumping the N02 edition, change the URL and cache directory in `preprocessor/src/track/mod.rs` and the `Cache N02 railway data` step in `.github/actions/build-worker/action.yml` together and compare the unmeasured count in the `preprocessor` log. `docs/architecture.md` (駅間の線路の長さ) has the details. - **TTS metadata** – `Station`, `StationNested`, `Line`, `LineNested`, `TrainType`, and `TrainTypeNested` expose `name_ipa` / `name_roman_ipa` plus `name_tts_segments` for multi-segment pronunciation output. Use `name_tts_segments` when clients need per-token SSML construction for mixed-language names such as `Kasai-Rinkai Park`. -- **Connected routes** – `connectedRoutes` finds transfer routes automatically, like a journey planner, using a frequency-based RAPTOR search in `stationapi/src/domain/route_search.rs`. Each rail line group is a pattern, station groups are the transfer nodes, and ride times come from `arrival_estimation`; bus lines are excluded. The cost adds a per-boarding wait by `TrainTypeKind` (limited express 15 min, express / high-speed rapid 5 min, others 3 min) and a 3-minute transfer walk — without the wait, infrequent limited expresses would beat the Yamanote Line. Rounds give the time/transfer Pareto set; alternatives come from re-searching with one leg's parallel line groups banned along that leg (at most 8 searches), and are dropped beyond 1.15 × best + 15 min or with two more transfers than the Pareto set. Alternative routes that stop at the same station group in two different legs (backtracking to re-board a banned train) are dropped; pass-through stations are not counted, and the Pareto routes of the first search are never dropped this way (otherwise a station `stationsByName` reports as reachable could get no route). Results are ranked by cost + 5 min per transfer and capped at 6. The time and transfer count stay internal (the API does not return them), so ordering happens on the server: `sortBy: ConnectedRouteSort` picks `Recommended` (the ranking above, the default when omitted), `ArrivalTime` (the estimated time `estimateArrivalTimes` also reports — excluding the first train's wait — then fewer transfers), or `TransferCount` (fewer transfers, then earlier arrival). `route_search::sort_journeys` only reorders the set `search` returned, with a stable sort so ties keep the recommended order; the set itself never depends on `sortBy`. The network (every rail line group plus its time estimates, about 190 ms natively) is built lazily into a `OnceLock` by `StationRepository::get_route_network` on the first `connectedRoutes` call, so other queries never pay for it. Each route is a list of `legs` shaped for the app's one-train-at-a-time flow: every leg carries its boarding and alighting `Station`, both on the line of the line group the search rode — so at a transfer the previous leg's alighting station and the next leg's boarding station may be different stations of one station group — `stationGroupIds`, the station groups from boarding to alighting in travel order including pass-through stations (the search's `JourneyLeg.station_group_ids` as-is — station groups rather than station IDs so the client can match them against whichever train type it picks, which may run on another line; a group appears twice on patterns such as the Oedo Line's Tochomae), and `trainTypes`, every train type usable on that leg (real `groupId`s, so the client picks one and calls `lineGroupStations`): it is exactly `routeTypes(boarding station group, alighting station group, alighting station's line)` (same dedup, same `lines`, same order — the use case calls `get_train_types`), because the search collapses parallel services such as local and rapid into one route and the app needs them to list types and default to the local. `viaLineId`, like `routeTypes`, is the line of the tapped search result and keeps only routes whose last leg arrives on that line. `estimateArrivalTimes` and `trainRoute` accept `legs: [RouteLegInput!]` (the `groupId` of the train type picked from each leg's `trainTypes`, plus the leg's `fromStation.id` and `toStation.id`) and then return values for the whole transfer route. A leg endpoint missing from the chosen line group is matched by station group (the picked local may stop at another line's station of the same group), preferring an exact `station_cd` and, among same-group candidates, the pair giving the shortest slice (through services list two stations of a junction group). ETA estimates each leg on its own line group only and chains them from the origin, adding the 3-minute walk and the next train type's wait at each transfer (the same allowance the search ranks by), returning one route with an empty `id`; `trainRoute` concatenates each leg's segments (each leg restarts at distance 0). Both slice legs with the same function, taking the shorter arc on loop lines, so their station sequences match. More than `MAX_RIDES` (6) legs — more than `connectedRoutes` ever returns — legs that do not connect, ends that differ from `fromStationId` / `toStationId`, or combining `legs` with `viaLineIds` / `directionId` / `lineGroupId` are errors. `docs/architecture.md` (乗換経路探索) has the details, and `docs/route-search.md` documents the search internals (data structures, the scan, pruning, alternatives, determinism). +- **Connected routes** – `connectedRoutes` finds transfer routes automatically, like a journey planner, using a frequency-based RAPTOR search in `stationapi/src/domain/route_search.rs`. Each rail line group is a pattern, station groups are the transfer nodes, and ride times come from `arrival_estimation`; bus lines are excluded. The cost adds a per-boarding wait by `TrainTypeKind` (limited express 15 min, express / high-speed rapid 5 min, others 3 min) and a 3-minute transfer walk — without the wait, infrequent limited expresses would beat the Yamanote Line. Rounds give the time/transfer Pareto set; alternatives come from re-searching with one leg's parallel line groups banned along that leg (at most 8 searches), and are dropped beyond 1.15 × best + 15 min or with two more transfers than the Pareto set. Alternative routes that stop at the same station group in two different legs (backtracking to re-board a banned train) are dropped; pass-through stations are not counted, and the Pareto routes of the first search are never dropped this way (otherwise a station `stationsByName` reports as reachable could get no route). Results are ranked by cost + 5 min per transfer and capped at 6. The time and transfer count stay internal (the API does not return them), so ordering happens on the server: `sortBy: ConnectedRouteSort` picks `Recommended` (the ranking above, the default when omitted), `ArrivalTime` (the search's own ride time — excluding the first train's wait, and estimated with the original calibration, so it differs from what `estimateArrivalTimes` reports — then fewer transfers), or `TransferCount` (fewer transfers, then earlier arrival). `route_search::sort_journeys` only reorders the set `search` returned, with a stable sort so ties keep the recommended order; the set itself never depends on `sortBy`. The network (every rail line group plus its time estimates, about 190 ms natively) is built lazily into a `OnceLock` by `StationRepository::get_route_network` on the first `connectedRoutes` call, so other queries never pay for it. Each route is a list of `legs` shaped for the app's one-train-at-a-time flow: every leg carries its boarding and alighting `Station`, both on the line of the line group the search rode — so at a transfer the previous leg's alighting station and the next leg's boarding station may be different stations of one station group — `stationGroupIds`, the station groups from boarding to alighting in travel order including pass-through stations (the search's `JourneyLeg.station_group_ids` as-is — station groups rather than station IDs so the client can match them against whichever train type it picks, which may run on another line; a group appears twice on patterns such as the Oedo Line's Tochomae), and `trainTypes`, every train type usable on that leg (real `groupId`s, so the client picks one and calls `lineGroupStations`): it is exactly `routeTypes(boarding station group, alighting station group, alighting station's line)` (same dedup, same `lines`, same order — the use case calls `get_train_types`), because the search collapses parallel services such as local and rapid into one route and the app needs them to list types and default to the local. `viaLineId`, like `routeTypes`, is the line of the tapped search result and keeps only routes whose last leg arrives on that line. `estimateArrivalTimes` and `trainRoute` accept `legs: [RouteLegInput!]` (the `groupId` of the train type picked from each leg's `trainTypes`, plus the leg's `fromStation.id` and `toStation.id`) and then return values for the whole transfer route. A leg endpoint missing from the chosen line group is matched by station group (the picked local may stop at another line's station of the same group), preferring an exact `station_cd` and, among same-group candidates, the pair giving the shortest slice (through services list two stations of a junction group). ETA estimates each leg on its own line group only and chains them from the origin, adding the 3-minute walk and the next train type's wait at each transfer (the same allowance the search ranks by), returning one route with an empty `id`; `trainRoute` concatenates each leg's segments (each leg restarts at distance 0). Both slice legs with the same function, taking the shorter arc on loop lines, so their station sequences match. More than `MAX_RIDES` (6) legs — more than `connectedRoutes` ever returns — legs that do not connect, ends that differ from `fromStationId` / `toStationId`, or combining `legs` with `viaLineIds` / `directionId` / `lineGroupId` are errors. `docs/architecture.md` (乗換経路探索) has the details, and `docs/route-search.md` documents the search internals (data structures, the scan, pruning, alternatives, determinism). - Changes to the published contract require coordinated updates to `schema/public.graphql`, the async-graphql types in `src/graphql/`, and, when the shape of a value changes, `stationapi/src/model.rs` and the DTO conversions. ## Version Control (Git) diff --git a/docs/architecture.md b/docs/architecture.md index cf821dd8..df63c905 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -481,23 +481,23 @@ input RouteLegInput { lineGroupId: Int! fromStationId: Int! toStationId: Int! - `Legacy` (省略時): 追加した時点 (#1568) のモデルです。最高速度と加減速は `dto::simulation::resolve_speed_profile` が決め、到着・出発の見込み (`arrivalCumulativeMinutes` / `departureCumulativeMinutes`) は `null` です。 - MobileApp のオートモードがこの値で走るので、値を変えません。速度の較正は、 + 配布済みの MobileApp のオートモードがこの値で走るので、値を変えません。速度の較正は、 到着時間推定の較正を求め直す前の表を `domain/legacy_speed_table.rs` に凍結して 使います。 -- `Estimated`: 到着時間推定 (`arrival_estimation`) のモデルで、MobileApp の GPX の - 生成が使います。返す駅列に推定を掛け、停車・通過、最高速度、加減速を推定が - 使った値に置き換えて、到着・出発の見込みを入れます。`legs` を渡したときの見込みは、 - 同じ `legs` を渡した `estimateArrivalTimes` と同じ組み立て方 (乗換の徒歩と待ち - 時間) ですが、較正と駅間の距離が違うので値は一致しません (下記)。バスの駅を含む - 経路は推定のモデルの対象外なので、`Legacy` と同じ値を返します。 +- `Estimated`: 到着時間推定 (`arrival_estimation`) のモデルで、MobileApp の + オートモードと GPX の生成が使います。返す駅列に推定を掛け、停車・通過、最高速度、 + 加減速を推定が使った値に置き換えて、到着・出発の見込みを入れます。見込みは、 + 同じ区間の `estimateArrivalTimes` と同じ値です (`legs` を渡したときは、同じ + `legs` を渡した `estimateArrivalTimes` と同じ値)。バスの駅を含む経路は推定の + モデルの対象外なので、`Legacy` と同じ値を返します。 2 つのモデルは、加減速、運転余裕率、停車時間、較正テーブル、駅間の距離が違い ます。`Estimated` は、線路の長さ (`connections`) がある駅間ではそれを走行距離に 使い、無い駅間だけ直線距離 × 迂回係数で見積もり、その距離で求め直した較正 -(`speed_table` / `segment_speed_table`) を使います。この求め直した計算を使うのは -`Estimated` だけです。`estimateArrivalTimes` と乗換経路探索 (`connectedRoutes`) は、 -元の計算 (直線距離 × 迂回係数と、`domain/legacy_speed_table.rs` の元の較正) の -ままで、ETA と経路検索の結果は変わりません。`Legacy` の値で台形の速度プロファイルを作って走らせると、 +(`speed_table` / `segment_speed_table`) を使います。`estimateArrivalTimes` も同じ +計算です。乗換経路探索 (`connectedRoutes`) の所要時間だけは、元の計算 (直線距離 × +迂回係数と、`domain/legacy_speed_table.rs` の元の較正) のままです。そのため、 +経路検索の所要時間と ETA は一致しません。`Legacy` の値で台形の速度プロファイルを作って走らせると、 `estimateArrivalTimes` より短い時間で走り切ります (#1709)。所要時間を推定に 合わせたいクライアントは、`Estimated` の見込みを使います。 @@ -515,7 +515,7 @@ input RouteLegInput { lineGroupId: Int! fromStationId: Int! toStationId: Int! ### 所要時間のベンチマーク (`travel_times/`) -`trainRoute` の `Estimated` の所要時間を、実際の列車の所要時間と比べる基準を +到着時間推定 (`estimateArrivalTimes` と `trainRoute` の `Estimated`) の所要時間を、実際の列車の所要時間と比べる基準を `travel_times/cases.csv` に置いています。速度の較正テーブルや一般則は、1 つの路線に合わせて変えると、同じ 規則を使うほかの路線の推定も変わります。変更の前後で全体の誤差を測るための仕組み です。 @@ -767,7 +767,7 @@ repository の実装がないメソッドは、空の結果ではなく `DomainE │ │ ├── entity/ # Station / Line / TrainType / Company ... │ │ ├── repository/ # 抽象インターフェース │ │ ├── arrival_estimation.rs -│ │ ├── legacy_speed_table.rs # 元の較正 (Estimated 以外のすべてが使う) +│ │ ├── legacy_speed_table.rs # 元の較正 (connectedRoutes と trainRoute の Legacy が使う) │ │ ├── route_search.rs # 乗換経路探索 (RAPTOR) │ │ ├── route_topology.rs # 所要時間を持たない系統網 (stationsByName の到達判定) │ │ ├── segment_speed_table.rs diff --git a/scripts/README.md b/scripts/README.md index 6c05e844..65213783 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -48,9 +48,9 @@ OSM データは [Open Database License (ODbL)](https://www.openstreetmap.org/co ## compute_speed_table.py 公開 GTFS 時刻表から、到着時間推定(`arrival_estimation.rs`)の速度較正テーブル -2 種類を再計算します。このテーブルを使うのは `trainRoute` の `Estimated` だけです -(`estimateArrivalTimes` と `connectedRoutes` は `legacy_speed_table.rs` の元の較正を -使います)。運動学モデルを Python で再現し、実ダイヤの所要時間を +2 種類を再計算します。このテーブルを使うのは `estimateArrivalTimes` と `trainRoute` の +`Estimated` です (`connectedRoutes` と `trainRoute` の `Legacy` は `legacy_speed_table.rs` +の元の較正を使います)。運動学モデルを Python で再現し、実ダイヤの所要時間を 再現する実効最高速度を二分探索でフィッティングします。 駅間の距離は、推定と同じく線路の長さ(`generated/connections.csv`)を使い、無い diff --git a/scripts/travel_time_report.py b/scripts/travel_time_report.py index db13b74a..4e9cc4dc 100644 --- a/scripts/travel_time_report.py +++ b/scripts/travel_time_report.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -"""実際の所要時間 (travel_times/cases.csv) に対する trainRoute の Estimated (MobileApp の -GPX の生成が使う推定) の誤差を、動いている Worker に問い合わせて Markdown で出す。 +"""実際の所要時間 (travel_times/cases.csv) に対する 到着時間推定 (trainRoute の +Estimated。estimateArrivalTimes と同じ値) の誤差を、動いている Worker に問い合わせて Markdown で出す。 CI の回帰テスト (src/travel_times.rs) は data/*.csv だけで動くので、生成データにしか 無い種別グループを飛ばし、線路の長さも持たない。本番と同じ生成データでの精度は、 diff --git a/src/graphql/enums.rs b/src/graphql/enums.rs index 54c85515..28dc046e 100644 --- a/src/graphql/enums.rs +++ b/src/graphql/enums.rs @@ -96,7 +96,8 @@ pub enum ConnectedRouteSort { // おすすめ順 (評価値 = 最初の列車の待ち時間を含む所要時間の見込み、に乗換 1 回あたり // 5 分を足した値の小さい順)。既定 Recommended, - // 到着の早い順 (estimateArrivalTimes の見込みと同じ所要時間)。同じなら乗換の少ない順 + // 到着の早い順 (探索の所要時間。元の較正で見積もるので estimateArrivalTimes の + // 見込みとは一致しない)。同じなら乗換の少ない順 ArrivalTime, // 乗換の少ない順。同じなら到着の早い順 TransferCount, diff --git a/src/repository.rs b/src/repository.rs index 3171df5e..f0333017 100644 --- a/src/repository.rs +++ b/src/repository.rs @@ -1322,16 +1322,11 @@ mod tests { .flat_map(|leg| leg.station_cds.iter().copied()) .collect(); assert_eq!(eta_ids, journey_ids); - // 累積は減らず、最後は探索の所要時間と (ほぼ) 一致する + // 累積は減らない。見込みは求め直した較正と線路の長さで出すので、元の較正で + // 見積もる探索の所要時間 (journey.total_seconds) とは一致しない assert!(eta.windows(2).all(|pair| { pair[1].cumulative_minutes >= pair[0].departure_cumulative_minutes - 1e-9 })); - let last = eta.last().unwrap().cumulative_minutes; - let expected = f64::from(journey.total_seconds) / 60.0; - assert!( - (last - expected).abs() < 1.0, - "eta {last} vs search {expected}" - ); // 乗換では徒歩の後に乗換先の列車を待ち、走行区間は距離 0 から始まる。 // 乗車駅の行は、前の区間の降車駅の行のすぐ後 let mut board = 0; @@ -1666,11 +1661,9 @@ mod tests { assert_eq!(route_ids, ids); } - /// Estimated の trainRoute は、同じ legs を渡した estimateArrivalTimes と同じ駅を - /// 同じ組み立て方 (乗換の徒歩と待ち時間) でつないだ見込みを返し、停車・加減速も - /// 推定のモデルにそろう。値は Estimated だけが求め直した較正と線路の長さを使う - /// ので、estimateArrivalTimes とは一致しない。Legacy は見込みを返さず、区間の値も - /// 変わらない。 + /// Estimated の trainRoute は、同じ legs を渡した estimateArrivalTimes と同じ + /// 見込みを返し、停車・加減速も推定のモデルにそろう。Legacy は見込みを返さず、 + /// 区間の値も変わらない。 #[test] fn estimated_connected_train_route_carries_the_estimate_arrival_times() { use stationapi::use_case::traits::query::QueryUseCase; @@ -1690,17 +1683,17 @@ mod tests { .unwrap(); assert_eq!(estimated.len(), eta.len()); - let mut previous_departure = 0.0; for (i, (segment, stop)) in estimated.iter().zip(&eta).enumerate() { assert_eq!( - segment.station.as_ref().map(|station| station.id as i32), - Some(stop.station_cd) + segment.arrival_cumulative_minutes, + Some(stop.cumulative_minutes) + ); + assert_eq!( + segment.departure_cumulative_minutes, + Some(stop.departure_cumulative_minutes) ); - let arrival = segment.arrival_cumulative_minutes.unwrap(); - let departure = segment.departure_cumulative_minutes.unwrap(); - assert!(arrival >= previous_departure - 1e-9 && departure >= arrival); - previous_departure = departure; assert_eq!(segment.stops, stop.stops_here); + assert_eq!(segment.max_speed, stop.max_speed_kmh / 3.6); assert_eq!(segment.max_acceleration, params.accel); assert_eq!(segment.max_deceleration, params.decel); // 駅と距離は Legacy と同じ @@ -1719,7 +1712,7 @@ mod tests { } /// lineGroupId で呼んだ Estimated の trainRoute は、その区間を 1 つの leg にした - /// Estimated の trainRoute と同じ見込みになる (環状線でない系統では同じ駅列を + /// estimateArrivalTimes と同じ見込みになる (環状線でない系統では同じ駅列を /// 同じ較正母数で推定するため)。#1709 の 2 経路で確かめる。 #[test] fn estimated_train_route_matches_a_single_leg_estimate() { @@ -1734,26 +1727,27 @@ mod tests { TrainRouteModel::Estimated, )) .unwrap(); - let single_leg = block_on(interactor.get_connected_train_route( - &[model::RouteLegRequest { + let eta = block_on(interactor.estimate_connected_route_arrival_times(&[ + model::RouteLegRequest { line_group_id, from_station_id: from, to_station_id: to, - }], - TrainRouteModel::Estimated, - )) + }, + ])) .unwrap(); - assert_eq!(segments.len(), single_leg.len()); - for (segment, leg_segment) in segments.iter().zip(&single_leg) { - assert_eq!(segment.station, leg_segment.station); - assert!(segment.arrival_cumulative_minutes.is_some()); + assert_eq!(segments.len(), eta.len()); + for (segment, stop) in segments.iter().zip(&eta) { + assert_eq!( + segment.station.as_ref().map(|station| station.id as i32), + Some(stop.station_cd) + ); assert_eq!( segment.arrival_cumulative_minutes, - leg_segment.arrival_cumulative_minutes + Some(stop.cumulative_minutes) ); assert_eq!( segment.departure_cumulative_minutes, - leg_segment.departure_cumulative_minutes + Some(stop.departure_cumulative_minutes) ); } } diff --git a/src/travel_times.rs b/src/travel_times.rs index 231ca0c1..f0cc0653 100644 --- a/src/travel_times.rs +++ b/src/travel_times.rs @@ -1,6 +1,6 @@ -//! 実際の所要時間 (`travel_times/cases.csv`) に対する `trainRoute` の `Estimated` -//! (MobileApp の GPX の生成が使う推定) の回帰の見張り。`estimateArrivalTimes` と -//! `connectedRoutes` は元の較正のままなので、ここでは測らない。 +//! 実際の所要時間 (`travel_times/cases.csv`) に対する到着時間推定 (`trainRoute` の +//! `Estimated`。`estimateArrivalTimes` と同じ値) の回帰の見張り。`connectedRoutes` の +//! 所要時間は元の較正のままなので、ここでは測らない。 //! //! 基準ごとに推定の所要時間を出し、実際の典型的な所要時間 (平日日中の中央値) から //! のずれを求める。記録した推定 (`travel_times/baseline.csv`) より悪くなった基準が @@ -156,8 +156,8 @@ fn estimate(case: &Case) -> Option { from_station_id: case.from_station_id, to_station_id: case.slice_end_station_id, }]; - // 見張るのは trainRoute の Estimated (GPX の生成が使う推定)。estimateArrivalTimes と - // connectedRoutes は元の較正のままで、推定の規則や較正を変えても動かない + // 見張るのは trainRoute の Estimated (estimateArrivalTimes と同じ値)。connectedRoutes の + // 所要時間は元の較正のままで、推定の規則や較正を変えても動かない let segments = block_on(crate::interactor().get_connected_train_route(&legs, TrainRouteModel::Estimated)) .unwrap_or_else(|e| panic!("{}: 推定できない: {e}", case.label)); diff --git a/stationapi/src/domain/arrival_estimation.rs b/stationapi/src/domain/arrival_estimation.rs index 1f732f60..22985f56 100644 --- a/stationapi/src/domain/arrival_estimation.rs +++ b/stationapi/src/domain/arrival_estimation.rs @@ -78,13 +78,13 @@ pub struct EstimatedStop { /// 速度の較正にどの表を使うか。 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum SpeedCalibration { - /// 元の較正 (`legacy_speed_table`。#1712 で求め直す前の表)。`estimateArrivalTimes`、 - /// `connectedRoutes` など、`trainRoute` の `Estimated` 以外はすべてこれを使う。 + /// 元の較正 (`legacy_speed_table`。#1712 で求め直す前の表)。`connectedRoutes` の + /// 所要時間が使う。 #[default] Original, /// 駅間の距離に線路の長さを使う前提で求め直した較正 (`speed_table` / - /// `segment_speed_table`)。`trainRoute` の `Estimated` (MobileApp の GPX の生成) - /// だけが使う。 + /// `segment_speed_table`)。到着見込み (`estimateArrivalTimes` と `trainRoute` の + /// `Estimated`) が使う。 Recalibrated, } diff --git a/stationapi/src/domain/legacy_speed_table.rs b/stationapi/src/domain/legacy_speed_table.rs index 9a1a5221..e730ada3 100644 --- a/stationapi/src/domain/legacy_speed_table.rs +++ b/stationapi/src/domain/legacy_speed_table.rs @@ -1,13 +1,12 @@ -//! 元の速度の較正テーブル。`trainRoute` の `Estimated` 以外のすべて -//! (`estimateArrivalTimes`、`connectedRoutes`、`trainRoute` の `Legacy` = MobileApp の -//! オートモード) が使う。 +//! 元の速度の較正テーブル。`connectedRoutes` の所要時間と `trainRoute` の `Legacy` +//! が使う。 //! //! `speed_table` / `segment_speed_table` は、駅間の距離に線路の長さを使う前提で -//! 求め直した (#1712)。求め直した較正は `trainRoute` の `Estimated` (MobileApp の GPX の -//! 生成) だけが使い、ほかの推定の値が変わらないよう、求め直す前の表 +//! 求め直した (#1712)。求め直した較正は到着見込み (`estimateArrivalTimes` と +//! `trainRoute` の `Estimated`) が使う。経路検索の結果が変わらないよう、求め直す前の表 //! (TrainLCD/StationAPI の `dev`、#1711 の時点) をここに凍結して持つ。 //! `scripts/compute_speed_table.py` はこのファイルを書き換えない。値を変えると -//! ETA・経路検索・オートモードが変わるので、変えないこと。 +//! 経路検索の結果が変わるので、変えないこと。 use crate::domain::segment_speed_table::segment_override_applies_to_kind; use crate::model::TrainTypeKind; diff --git a/stationapi/src/domain/route_search.rs b/stationapi/src/domain/route_search.rs index 55383db4..660c9fe2 100644 --- a/stationapi/src/domain/route_search.rs +++ b/stationapi/src/domain/route_search.rs @@ -145,8 +145,9 @@ pub enum JourneySort { /// [`RouteNetwork::search`] が返す順そのもの。 #[default] Recommended, - /// 到着の早い順。[`Journey::total_seconds`] (`estimateArrivalTimes` の見込みと - /// 同じく、最初の列車の待ち時間を含まない) の小さい順で、同じなら乗換の少ない順。 + /// 到着の早い順。[`Journey::total_seconds`] (最初の列車の待ち時間を含まない) の + /// 小さい順で、同じなら乗換の少ない順。所要時間は元の較正で見積もるので、 + /// `estimateArrivalTimes` の見込みとは一致しない。 ArrivalTime, /// 乗換の少ない順。同じなら到着の早い順。 TransferCount, diff --git a/stationapi/src/domain/segment_speed_table.rs b/stationapi/src/domain/segment_speed_table.rs index a29765b4..c9873fa8 100644 --- a/stationapi/src/domain/segment_speed_table.rs +++ b/stationapi/src/domain/segment_speed_table.rs @@ -6,8 +6,9 @@ //! この残差を原理的に解消できないため、公開 GTFS 時刻表から隣接駅ペア単位で //! フィットした実効速度をここで上書きする。 //! -//! `trainRoute` の `Estimated` の推定 (`SpeedCalibration::Recalibrated`) だけが -//! 参照する。ほかの推定は、求め直す前の表 (`legacy_speed_table`) を使う。 +//! `estimateArrivalTimes` と `trainRoute` の `Estimated` の推定 +//! (`SpeedCalibration::Recalibrated`) が参照する。`connectedRoutes` は、求め直す前の表 +//! (`legacy_speed_table`) を使う。 //! //! 生成は `scripts/compute_speed_table.py --apply`(路線単位テーブルと同時生成)。 //! 駅間の純走行時間(次駅の到着時刻が別記録されていれば「出発 → 次駅到着」、 diff --git a/stationapi/src/domain/speed_table.rs b/stationapi/src/domain/speed_table.rs index a357dfe2..30b57858 100644 --- a/stationapi/src/domain/speed_table.rs +++ b/stationapi/src/domain/speed_table.rs @@ -9,9 +9,9 @@ //! //! 値の意味は「その路線・種別での実効巡航速度(km/h)」。理論上の車両性能では //! なく、時刻表所要時間を運動学モデルで再現する値として較正している。 -//! `trainRoute` の `Estimated` の推定 (`SpeedCalibration::Recalibrated`) だけが -//! 参照する。`estimateArrivalTimes`・`connectedRoutes`・`trainRoute` の `Legacy` は、 -//! 求め直す前の表 (`legacy_speed_table`) を使う。 +//! `estimateArrivalTimes` と `trainRoute` の `Estimated` の推定 +//! (`SpeedCalibration::Recalibrated`) が参照する。`connectedRoutes`・`trainRoute` の +//! `Legacy` は、求め直す前の表 (`legacy_speed_table`) を使う。 //! //! エントリ追加の指針: //! - 公表運転速度(例: 京急快特 120km/h、スカイライナー 160km/h)を起点にし、 @@ -33,8 +33,8 @@ use crate::model::TrainTypeKind; /// 距離が直線 × 迂回係数だった頃の値は、距離の水増しを速度で打ち消していたので、 /// 線路の長さへ替えたときに求め直した。小田急線 (快速急行) は一般則で典型値に /// 近づいたので外した。つくばエクスプレスと都営大江戸線は GTFS の自動較正に任せる。 -/// `estimateArrivalTimes`・`connectedRoutes`・`trainRoute` の `Legacy` は、求め直す前の -/// 値を `legacy_speed_table` で使い続ける。 +/// `connectedRoutes`・`trainRoute` の `Legacy` は、求め直す前の値を +/// `legacy_speed_table` で使い続ける。 const LINE_SPEED_OVERRIDES: &[(i32, TrainTypeKind, f64)] = &[ // 総武快速線: 最高 130km/h の別線を走る。StationAPI の路線には快速の停車駅しか // 無く、通過駅が無いので推定は各停 (Default) として扱う。一般則の 80km/h では diff --git a/stationapi/src/model.rs b/stationapi/src/model.rs index e876e9c5..c4af40ff 100644 --- a/stationapi/src/model.rs +++ b/stationapi/src/model.rs @@ -347,7 +347,7 @@ pub struct RouteLeg { #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum TrainRouteModel { /// 追加した時点 (#1568) のモデル。`dto::simulation::resolve_speed_profile` の - /// 最高速度・加減速を返し、到着・出発の見込みは返さない。MobileApp の + /// 最高速度・加減速を返し、到着・出発の見込みは返さない。配布済みの MobileApp の /// オートモードが使う。既定。 #[default] Legacy, diff --git a/stationapi/src/use_case/interactor/query.rs b/stationapi/src/use_case/interactor/query.rs index 4ac221e9..c74b8242 100644 --- a/stationapi/src/use_case/interactor/query.rs +++ b/stationapi/src/use_case/interactor/query.rs @@ -947,7 +947,7 @@ where &self, legs: &[model::RouteLegRequest], ) -> Result, UseCaseError> { - self.estimate_legs(legs, false).await + self.estimate_legs(legs).await } async fn get_connected_train_route( @@ -982,11 +982,10 @@ where // 区間ごとに別の列車なので、通過駅の有無や距離の起点も区間ごとに数える segments.extend(self.train_route_segments(sliced, leg.line_group_id).await?); } - // 見込みは estimateArrivalTimes に同じ legs を渡したときと同じ組み立て方 - // (乗換の徒歩と待ち時間の見込みも含む) で、較正と駅間の距離だけを Estimated の - // もの (求め直した較正と線路の長さ) にする。バスは推定のモデルの対象外 + // 見込みは estimateArrivalTimes に同じ legs を渡したときと同じ値にする + // (乗換の徒歩と待ち時間の見込みも含む)。バスは推定のモデルの対象外 if estimated_model && !has_bus { - let estimated = self.estimate_legs(legs, true).await?; + let estimated = self.estimate_legs(legs).await?; apply_estimated_model(&mut segments, &estimated)?; } Ok(segments) @@ -1133,16 +1132,17 @@ where .await?; let route_row_tree_map = self.build_route_tree_map(&stops); - let params = EstimationParams::default(); + let params = arrival_estimation_params(); let mut result: Vec = Vec::new(); for group_stops in route_row_tree_map.values() { + let track = self.track_distances_of(group_stops).await?; if let Some(segment) = estimate_group_segment( group_stops, SegmentEndpoints::exact(from_station_id, to_station_id), direction_id.is_some(), ¶ms, - None, + Some(&track), ) { result.extend(segment); } @@ -1159,21 +1159,15 @@ where TR: TrainTypeRepository, CR: CompanyRepository, { - /// 区間 (`legs`) をつないだ経路の到着見込み。`estimated_model` が真なら - /// `trainRoute` の `Estimated` 用で、求め直した較正と線路の長さで推定する。 - /// 偽なら `estimateArrivalTimes` 用で、元の較正と直線距離 × 迂回係数で推定する - /// (`connectedRoutes` の所要時間と同じ見積もり)。 + /// 区間 (`legs`) をつないだ経路の到着見込み。`estimateArrivalTimes` と + /// `trainRoute` の `Estimated` が使い、求め直した較正と線路の長さで推定する。 + /// `connectedRoutes` の所要時間は元の較正で見積もるので、この値とは一致しない。 async fn estimate_legs( &self, legs: &[model::RouteLegRequest], - estimated_model: bool, ) -> Result, UseCaseError> { let group_of = self.validate_route_legs(legs).await?; - let params = if estimated_model { - estimated_model_params() - } else { - EstimationParams::default() - }; + let params = arrival_estimation_params(); let walk_minutes = f64::from(route_search::TRANSFER_WALK_SECONDS) / 60.0; let mut result: Vec = Vec::new(); @@ -1185,11 +1179,7 @@ where .get_by_line_group_id(leg.line_group_id) .await?; let group_stops: Vec<&Station> = stations.iter().collect(); - let track = if estimated_model { - Some(self.track_distances_of(&group_stops).await?) - } else { - None - }; + let track = self.track_distances_of(&group_stops).await?; let (from_group, to_group) = (group_of[&leg.from_station_id], group_of[&leg.to_station_id]); let segment = estimate_group_segment( @@ -1201,12 +1191,12 @@ where }, false, ¶ms, - track.as_ref(), + Some(&track), ) .ok_or_else(|| leg_not_found(leg))?; // 乗換では、乗換駅に歩いて着いた時刻を到着、乗換先の列車を待った後を - // 出発とする。見込みは connectedRoutes の所要時間と同じ (最初の列車の + // 出発とする。徒歩と待ち時間の見込みは connectedRoutes と同じ (最初の列車の // 待ち時間は含めない) let (board_arrival, board_departure) = match result.last() { Some(previous) if index > 0 => { @@ -2105,15 +2095,15 @@ fn estimate_train_route_slice( Some(estimate_arrival_minutes_with_track( &stops, &route, - &estimated_model_params(), + &arrival_estimation_params(), Some(track), )) } -/// `trainRoute` の `Estimated` だけが使う推定のパラメータ。駅間の距離に線路の長さを -/// 使う前提で求め直した較正を使う。ほかの推定 (`estimateArrivalTimes`、 -/// `connectedRoutes`) は元の較正 ([`EstimationParams::default`]) のまま。 -fn estimated_model_params() -> EstimationParams { +/// 到着見込み (`estimateArrivalTimes` と `trainRoute` の `Estimated`) の推定の +/// パラメータ。駅間の距離に線路の長さを使う前提で求め直した較正を使う。 +/// `connectedRoutes` の所要時間は元の較正 ([`EstimationParams::default`]) のまま。 +fn arrival_estimation_params() -> EstimationParams { EstimationParams { speed_calibration: SpeedCalibration::Recalibrated, ..EstimationParams::default() @@ -6502,7 +6492,7 @@ mod tests { let slice: Vec<&Station> = group[2..=6].iter().collect(); let whole: Vec<&Station> = group.iter().collect(); - let params = estimated_model_params(); + let params = arrival_estimation_params(); let track = interactor.track_distances_of(&whole).await.unwrap(); assert!( !track.is_empty(), diff --git a/travel_times/README.md b/travel_times/README.md index 0b214027..cdc330f0 100644 --- a/travel_times/README.md +++ b/travel_times/README.md @@ -1,8 +1,8 @@ # travel_times/ -`trainRoute` の `Estimated` (MobileApp の GPX の生成が使う到着時間推定) の所要時間を、 -実際の列車の所要時間と比べるための基準を置く場所です。`estimateArrivalTimes` と -`connectedRoutes` は元の較正のままなので、ここでは測りません。速度の較正テーブルや一般則は、 +到着時間推定 (`estimateArrivalTimes` と、同じ値を返す `trainRoute` の `Estimated`) の +所要時間を、実際の列車の所要時間と比べるための基準を置く場所です。`connectedRoutes` の +所要時間は元の較正のままなので、ここでは測りません。速度の較正テーブルや一般則は、 1 つの路線に合わせて変えると、同じ規則を使うほかの路線の推定も変わります。 変更の前後で全体の誤差を測り、局所的な合わせ込みで全体が崩れないようにします。