What Happens When You Feed an H200 GPU Millions of Transit Routes?
Executive Summary
What Happens When You Feed an H200 GPU Millions of Transit Routes? Building a Modern Transportation Intelligence Stack

Executive Summary
This case study documents a project pipeline that turns VTA GTFS schedule data into a route-ranking system and presents the results in a Next.js demo. The work combined large-scale candidate generation, GPU-accelerated XGBoost training, JSON export, and a web handoff designed to make the model output usable by end users in a Web UI.
The goal was to test whether public transit schedules could be transformed into a practical ranking engine and surfaced through a modern, lightweight product experience.
Workflow Overview

Figure 1: Transit AI workflow from GTFS ingest to the web UI.
Background
Background and Motivation: Building a Transit AI Route Optimization Platform on NVIDIA H200
Public transportation systems generate enormous amounts of operational and scheduling data every day, yet much of this information remains underutilized from an optimization and decision-support perspective. Most transit agencies publish their schedules through the General Transit Feed Specification (GTFS), an open standard that contains route definitions, stop locations, trip schedules, and timing information. While GTFS data is widely available, transforming raw transit schedules into an intelligent route recommendation engine remains a challenging engineering problem.
The objective of this project was to explore whether publicly available GTFS data could be converted into a practical AI-powered route optimization system using modern machine learning techniques and GPU acceleration. Rather than building a full-scale transit planner from the outset, the project focused on validating a complete end-to-end workflow: ingesting public transit data, generating route candidates, training a machine learning model, and serving optimized recommendations through a modern web application.
The project used the Santa Clara Valley Transportation Authority (VTA) GTFS dataset as a representative transit network. GTFS feeds provide detailed information about stops, routes, trips, and stop times, but they are fundamentally schedule-oriented rather than optimization-oriented. A significant engineering effort was therefore required to transform schedule data into machine-learning-ready training examples. Millions of origin-destination route candidates were generated by analyzing stop sequences across thousands of trips. Additional features such as travel time, departure hour, stop count, route identifiers, estimated distance, and environmental impact metrics were then engineered to support route ranking.
A key motivation for this work was to evaluate the feasibility of using modern AI infrastructure to accelerate transportation intelligence applications. Recent advances in GPU computing have dramatically reduced the time required to train large machine learning models on structured datasets. By leveraging an NVIDIA H200 GPU hosted on a Leaseweb server, the project was able to process hundreds of thousands of route samples and train an XGBoost model within minutes rather than hours. This enabled rapid experimentation, feature engineering, and model iteration while maintaining an interactive development workflow.
Beyond technical experimentation, the project also explored a broader architectural concept: separating offline AI training from online recommendation delivery. Instead of deploying a heavyweight inference service, the trained model produced route rankings that were exported into lightweight JSON artifacts. These artifacts were then consumed directly by a Next.js web application, allowing users to interact with route recommendations without requiring live model inference. This architecture significantly simplifies deployment, reduces infrastructure costs, and enables rapid prototyping of transportation intelligence products.
The long-term motivation extends beyond route ranking itself. The same architecture could eventually support real-time transit optimization, dynamic dispatching, demand forecasting, multimodal transportation planning, carbon-emission reduction analysis, and smart-city decision support systems. By combining open transit data, GPU-accelerated machine learning, and modern web technologies, this project demonstrates a practical pathway toward building the next generation of AI-powered transportation platforms.
Project Motivation
Rather than start with live trip planning, the project focused on proving the data pipeline. If GTFS data could be normalized, scored, and packaged cleanly, it would create a strong foundation for future real-time and optimization features.
GTFS Data Acquisition
I downloaded VTA’s public GTFS bundle and loaded the core schedule tables: stops, routes, trips, and stop_times. Those files provided the structural backbone for the entire feature engineering pipeline.
Generating Route Candidates
The key transformation was converting ordered stop sequences into origin-destination pairs. That step expanded the schedule data into roughly 2.68 million route candidates across more than 11,000 trips, turning static transit records into model-ready examples.
Debugging and Engineering Challenges
As with most data projects, the hardest problems were operational rather than conceptual. The notebook surfaced list-versus-DataFrame mistakes, missing intermediate frames, GTFS identifier mismatches, feature drift during inference, and GPU prediction warnings. Each issue helped harden the pipeline and clarified where the project needed more defensive data handling.
Feature Engineering
The feature set blended route structure with trip context: origin and destination stops, route ID, departure hour, stop count, travel time, estimated distance, and emissions estimates. That mix gave the model enough signal to rank routes without making the system overly complex.
GPU Model Training
XGBoost with CUDA acceleration kept experimentation fast enough to iterate comfortably. A 500,000-row sample was sufficient for model tuning before the full candidate set was scored.
Model Scoring and Route Ranking
The trained model generated AI scores that were later combined with optimization logic. In practice, a pure score could favor routes that looked strong numerically but were less useful operationally, so the final system used a hybrid ranking strategy.
JSON Export and Website Integration
The notebook exported stops.json, ranked_routes.json, and model_metrics.json. Those artifacts were copied into the Next.js application and consumed by the Transit Demo page, turning the notebook into a usable product pipeline.
Lessons Learned and Roadmap
The project demonstrated a complete offline-training and online-serving architecture. The most obvious next steps are GTFS-Realtime support, shapes.txt visualization, OpenTripPlanner integration, and demand forecasting experiments, etc.
Appendix
Jupyter Notebook for the project
import requests, zipfile, os
url = "https://gtfs.vta.org/gtfs_vta.zip"
os.makedirs("data", exist_ok=True)
r = requests.get(url)
open("data/vta.zip","wb").write(r.content)
with zipfile.ZipFile("data/vta.zip") as z:
z.extractall("data/vta")
import pandas as pd
stops = pd.read_csv("data/vta/stops.txt")
routes = pd.read_csv("data/vta/routes.txt")
trips = pd.read_csv("data/vta/trips.txt")
stop_times = pd.read_csv("data/vta/stop_times.txt")
def gtfs_time_to_sec(t):
h,m,s = map(int,str(t).split(":"))
return h*3600+m*60+s
stop_times["arrival_sec"] = stop_times["arrival_time"].apply(gtfs_time_to_sec)
stop_times["departure_sec"] = stop_times["departure_time"].apply(gtfs_time_to_sec)
sample_trips = stop_times["trip_id"].drop_duplicates().sample(
min(3000, stop_times["trip_id"].nunique()),
random_state=42
)
st = stop_times[
stop_times["trip_id"].isin(sample_trips)
].copy()
st = st.sort_values(
["trip_id", "stop_sequence"]
)
import pandas as pd
import numpy as np
# Make sure seconds are numeric
stop_times["arrival_sec"] = pd.to_numeric(stop_times["arrival_sec"], errors="coerce")
stop_times["departure_sec"] = pd.to_numeric(stop_times["departure_sec"], errors="coerce")
stop_times["stop_sequence"] = pd.to_numeric(stop_times["stop_sequence"], errors="coerce")
clean_st = stop_times.dropna(
subset=["trip_id", "stop_id", "arrival_sec", "departure_sec", "stop_sequence"]
).copy()
clean_st = clean_st.sort_values(["trip_id", "stop_sequence"])
print("clean_st shape:", clean_st.shape)
print("unique trips:", clean_st["trip_id"].nunique())
# Test one trip
one_trip_id = clean_st["trip_id"].iloc[0]
one_trip = clean_st[clean_st["trip_id"] == one_trip_id].sort_values("stop_sequence")
one_trip[["trip_id", "stop_id", "arrival_time", "departure_time", "arrival_sec", "departure_sec", "stop_sequence"]].head(10)
pairs_list = []
for trip_id, g in clean_st.groupby("trip_id"):
g = g.sort_values("stop_sequence").reset_index(drop=True)
if len(g) < 2:
continue
rows = g[[
"trip_id",
"stop_id",
"arrival_sec",
"departure_sec",
"stop_sequence"
]].to_dict("records")
for i in range(len(rows) - 1):
for j in range(i + 1, min(i + 8, len(rows))):
a = rows[i]
b = rows[j]
travel_time = float(b["arrival_sec"]) - float(a["departure_sec"])
if travel_time > 0:
pairs_list.append({
"trip_id": trip_id,
"origin_stop_id": a["stop_id"],
"dest_stop_id": b["stop_id"],
"origin_seq": int(a["stop_sequence"]),
"dest_seq": int(b["stop_sequence"]),
"depart_sec": float(a["departure_sec"]),
"arrive_sec": float(b["arrival_sec"]),
"travel_time_sec": travel_time,
"num_stops": int(b["stop_sequence"]) - int(a["stop_sequence"])
})
pairs = pd.DataFrame(pairs_list)
print("pairs shape:", pairs.shape)
pairs.head()
trip_routes = trips[["trip_id", "route_id", "service_id"]].drop_duplicates()
pairs = pairs.merge(
trip_routes,
on="trip_id",
how="left"
)
pairs.head()
pairs["origin_stop_id"] = pairs["origin_stop_id"].astype(str)
pairs["dest_stop_id"] = pairs["dest_stop_id"].astype(str)
stops["stop_id"] = stops["stop_id"].astype(str)
stop_cols = [
"stop_id",
"stop_name",
"stop_lat",
"stop_lon"
]
pairs = pairs.merge(
stops[stop_cols].rename(columns={
"stop_id":"origin_stop_id",
"stop_name":"origin_name",
"stop_lat":"origin_lat",
"stop_lon":"origin_lon"
}),
on="origin_stop_id",
how="left"
)
stops = pd.read_csv(
"data/vta/stops.txt",
dtype={"stop_id": str}
)
stop_times = pd.read_csv(
"data/vta/stop_times.txt",
dtype={
"stop_id": str,
"trip_id": str
}
)
trips = pd.read_csv(
"data/vta/trips.txt",
dtype={
"trip_id": str,
"route_id": str
}
)
# For fast demo training
MAX_ROWS = 500_000
if len(pairs) > MAX_ROWS:
pairs_train_df = pairs.sample(MAX_ROWS, random_state=42).copy()
else:
pairs_train_df = pairs.copy()
print(pairs_train_df.shape)
pairs_train_df["hour"] = (pairs_train_df["depart_sec"] // 3600) % 24
pairs_train_df["straight_dist_km"] = (
np.sqrt(
(pairs_train_df["origin_lat"] - pairs_train_df["dest_lat"]) ** 2 +
(pairs_train_df["origin_lon"] - pairs_train_df["dest_lon"]) ** 2
) * 111
)
pairs_train_df = pairs_train_df.dropna()
pairs_train_df["distance_bucket"] = pd.qcut(
pairs_train_df["straight_dist_km"].rank(method="first"),
q=10,
labels=False
)
group_median = pairs_train_df.groupby(
["distance_bucket", "hour"]
)["travel_time_sec"].transform("median")
pairs_train_df["is_good_route"] = (
pairs_train_df["travel_time_sec"] <= group_median
).astype(int)
pairs_train_df["is_good_route"].value_counts(normalize=True)
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import OrdinalEncoder
from sklearn.metrics import classification_report, roc_auc_score
import xgboost as xgb
import time
features = [
"origin_stop_id",
"dest_stop_id",
"route_id",
"hour",
"num_stops",
"straight_dist_km",
"depart_sec",
]
cat_cols = [
"origin_stop_id",
"dest_stop_id",
"route_id",
]
X = pairs_train_df[features].copy()
y = pairs_train_df["is_good_route"].copy()
enc = OrdinalEncoder(
handle_unknown="use_encoded_value",
unknown_value=-1
)
X[cat_cols] = enc.fit_transform(X[cat_cols].astype(str))
X_train, X_test, y_train, y_test = train_test_split(
X,
y,
test_size=0.2,
random_state=42,
stratify=y
)
print(X_train.shape, X_test.shape)
model = xgb.XGBClassifier(
n_estimators=500,
max_depth=8,
learning_rate=0.05,
subsample=0.9,
colsample_bytree=0.9,
tree_method="hist",
device="cuda",
eval_metric="logloss",
random_state=42,
)
start = time.time()
model.fit(
X_train,
y_train,
eval_set=[(X_test, y_test)],
verbose=50
)
train_seconds = time.time() - start
print("Training seconds:", train_seconds)
pred_proba = model.predict_proba(X_test)[:, 1]
pred = (pred_proba >= 0.5).astype(int)
auc = roc_auc_score(y_test, pred_proba)
print("AUC:", auc)
print(classification_report(y_test, pred))
X_sample_score = pairs_train_df[features].copy()
X_sample_score[cat_cols] = enc.transform(X_sample_score[cat_cols].astype(str))
pairs_train_df["aiScore"] = model.predict_proba(X_sample_score)[:, 1]
pairs_train_df[[
"origin_name",
"dest_name",
"route_id",
"hour",
"travel_time_sec",
"aiScore"
]].sort_values("aiScore", ascending=False).head(10)
pairs["hour"] = (pairs["depart_sec"] // 3600) % 24
pairs["straight_dist_km"] = (
np.sqrt(
(pairs["origin_lat"] - pairs["dest_lat"]) ** 2 +
(pairs["origin_lon"] - pairs["dest_lon"]) ** 2
) * 111
)
pairs = pairs.dropna(subset=[
"origin_stop_id",
"dest_stop_id",
"route_id",
"hour",
"num_stops",
"straight_dist_km",
"depart_sec"
]).copy()
missing = [c for c in features if c not in pairs.columns]
print("missing:", missing)
print("pairs shape:", pairs.shape)
import numpy as np
import xgboost as xgb
booster = model.get_booster()
def score_in_chunks(df, chunk_size=250_000):
scores = []
for start in range(0, len(df), chunk_size):
end = min(start + chunk_size, len(df))
chunk = df.iloc[start:end].copy()
X_chunk = chunk[features].copy()
X_chunk[cat_cols] = enc.transform(X_chunk[cat_cols].astype(str))
dchunk = xgb.DMatrix(X_chunk)
chunk_scores = booster.predict(dchunk)
scores.append(chunk_scores)
print(f"Scored rows {start:,} to {end:,}")
return np.concatenate(scores)
pairs["aiScore"] = score_in_chunks(pairs)
pairs["travelMinutes"] = (pairs["travel_time_sec"] / 60).round(1)
pairs["distanceKm"] = pairs["straight_dist_km"].round(2)
pairs["carCo2KgEst"] = (pairs["distanceKm"] * 0.25).round(2)
pairs["transitCo2KgEst"] = (pairs["carCo2KgEst"] * 0.40).round(2)
pairs["co2SavedKg"] = (pairs["carCo2KgEst"] - pairs["transitCo2KgEst"]).round(2)
pairs["aiScore"] = pairs["aiScore"].round(4)
import os, json
from datetime import datetime, timezone
export_dir = "website_export"
os.makedirs(export_dir, exist_ok=True)
stops_export = stops[[
"stop_id",
"stop_name",
"stop_lat",
"stop_lon"
]].rename(columns={
"stop_id": "stopId",
"stop_name": "stopName",
"stop_lat": "lat",
"stop_lon": "lon"
})
stops_export.to_json(
f"{export_dir}/stops.json",
orient="records",
indent=2
)
routes_export = pairs[[
"origin_stop_id",
"dest_stop_id",
"origin_name",
"dest_name",
"route_id",
"hour",
"travelMinutes",
"num_stops",
"distanceKm",
"aiScore",
"co2SavedKg",
"origin_lat",
"origin_lon",
"dest_lat",
"dest_lon"
]].rename(columns={
"origin_stop_id": "originStopId",
"dest_stop_id": "destinationStopId",
"origin_name": "originName",
"dest_name": "destinationName",
"route_id": "routeId",
"num_stops": "numStops",
"origin_lat": "originLat",
"origin_lon": "originLon",
"dest_lat": "destinationLat",
"dest_lon": "destinationLon"
})
routes_export = routes_export.sort_values(
["aiScore", "travelMinutes"],
ascending=[False, True]
)
routes_export.to_json(
f"{export_dir}/ranked_routes.json",
orient="records",
indent=2
)
metrics = {
"model": "XGBoost",
"trainingHardware": "NVIDIA H200",
"dataset": "VTA GTFS public schedule data",
"trainingRows": int(len(X_train)),
"testRows": int(len(X_test)),
"totalCandidateRoutes": int(len(pairs)),
"auc": float(round(auc, 4)),
"trainingSeconds": float(round(train_seconds, 2)),
"generatedAt": datetime.now(timezone.utc).isoformat(),
"dataStatus": "real_gpu_training_output"
}
with open(f"{export_dir}/model_metrics.json", "w") as f:
json.dump(metrics, f, indent=2)
print(os.listdir(export_dir))
pd.read_json("website_export/ranked_routes.json").head()
json.load(open("website_export/model_metrics.json"))
{'model': 'XGBoost',
'trainingHardware': 'NVIDIA H200',
'dataset': 'VTA GTFS public schedule data',
'trainingRows': 400000,
'testRows': 100000,
'totalCandidateRoutes': 2684414,
'auc': 0.9614,
'trainingSeconds': 2.04,
'generatedAt': '2026-06-09T01:25:13.662754+00:00',
'dataStatus': 'real_gpu_training_output'}
메타데이터
- post_id
- 331603bf0e4f
- slug
- what-happens-when-you-feed-an-h200-gpu-millions-of-transit-routes-331603bf0e4f
- url
- https://medium.com/@frankwangblock/what-happens-when-you-feed-an-h200-gpu-millions-of-transit-routes-331603bf0e4f
- canonical_url
- https://medium.com/@frankwangblock/what-happens-when-you-feed-an-h200-gpu-millions-of-transit-routes-331603bf0e4f
- author_url
- https://medium.com/@frankwangblock
- status
- ok
- fetched_at
- 2026-06-13 16:00:06