Where it comes from

How to answer

“Unreliable” is the whole question. A rider means one of several different failures, and each needs different data and a different fix, so run the decomposition method with that word at the center. Here is what the method looks like on a bus network:

  1. Find out which failure riders mean. Ask riders and dispatchers what they mean by it: long, unpredictable waits on frequent routes; buses arriving together; trips that never run; buses leaving a stop early; full buses passing people by. Ask what the agency measures today, and who asked for this.
  2. Name who reports reliability, and pick a measure per route type. The sponsor (the general manager or the head of operations) reports a number to the board; dispatchers and street supervisors act on the street; schedulers own the timetable; the garages assign operators. Measure what riders feel: wait beyond the scheduled wait on frequent routes, adherence to the timetable on infrequent ones, and missed trips everywhere. Ridership by route is the outcome.
  3. Find every record that timestamps a bus. The published GTFS schedule, stop-level arrival events from automatic vehicle location, passenger counters, dispatch logs of canceled trips, operator assignments, complaints. Name an owner for each.
  4. Spend the week testing one explanation. Ride and sit with dispatch, get the data, compute the measure for a handful of routes, and check whether the routes that lost riders are the least reliable ones before you build.
  5. Build one view a dispatcher uses on a live shift, such as bunching on one corridor, not a rider app.
  6. Name what would fool the numbers. Missing location data that looks like a missed trip, detours, schedule padding that makes on-time numbers look better while riders wait longer.

Say out loud what you would query first and what its columns are. Naming the table and its columns shows that “the data” means a table to you.

The trap is opening with a real-time arrival app. It predicts a bad service more accurately and changes nothing about why the bus is late.

Follow-ups

What the interviewer may ask next, once your first answer is on the table.

  • Which timestamp table would you query first, and what are its columns?
  • Many of the agency’s buses have no automatic vehicle location. What changes?
  • Ridership fell but on-time performance did not. What do you tell the sponsor?

Where answers go wrong

  • Proposes a real-time arrival app before defining ‘unreliable’ with riders and dispatchers.

Answer this in two minutes

Write the answer you would say out loud. The clock starts with your first word.

Two minutes

Compare with the model answer

Model answer

Clarify. Before anything else, I’d ask the sponsor what “riders are leaving” is based on, which routes lost the most, and what the agency reports as reliability today. Then I’d spend the first morning riding two of those routes and the afternoon in the dispatch room, asking dispatchers what they do when buses bunch. I’m listening for which failure riders mean: bunching, missed trips, or early departures.

Stakeholders. The head of operations sponsors this and reports on-time performance to the board, so excess wait will make their number look worse; I’d agree that with them on day one, not on Friday. Dispatchers would use the slice and street supervisors carry it out. Schedulers own the timetable and terminal recovery time. The garages and the operators’ union care about what holding does to breaks and overtime, so I’d meet the union representative this week, before anyone asks an operator to hold.

Metrics. On frequent routes, riders don’t read a timetable, so on-time performance is the wrong measure. I’d use excess wait: the average wait riders experience, minus the wait the schedule promises. On infrequent routes, timetable adherence, early departures counted separately because they strand people. Everywhere, the share of scheduled trips that never ran.

Inputs. Stop events come from the ITS team, the GTFS schedule from scheduling, the cancellation log from dispatch and boardings from the passenger counters, whose owner I’d ask for on Monday. The first table I’d query is stop-level events from the location system. At ~600 buses running ~15 trips of ~40 stops a day, that is roughly 360k rows a day, so a month of it fits in Postgres on a laptop.

-- stop_events(service_date, route_id, direction_id, stop_id, trip_id,
--             vehicle_id, block_id, scheduled_departure, actual_departure)
-- scheduled: every scheduled trip from GTFS stop_times, expanded by date
with actual as (
  select route_id, extract(epoch from actual_departure - lag(actual_departure) over w) / 60 as h
  from stop_events
  window w as (partition by service_date, route_id, direction_id, stop_id order by actual_departure)
), planned as (
  select route_id, extract(epoch from scheduled_departure - lag(scheduled_departure) over w) / 60 as h
  from scheduled
  where service_date in (select service_date from stop_events)  -- only days we observed
  window w as (partition by service_date, route_id, direction_id, stop_id order by scheduled_departure)
)
select a.route_id,
       sum(a.h ^ 2) / (2 * sum(a.h)) - p.sched_wait as excess_wait_min
from actual a
join (select route_id, sum(h ^ 2) / (2 * sum(h)) as sched_wait
      from planned where h is not null group by route_id) p using (route_id)
where a.h is not null
  and p.sched_wait < 6          -- frequent routes: scheduled headway under 12 minutes
group by a.route_id, p.sched_wait
order by excess_wait_min desc;

For riders who turn up at random, as they do on frequent routes, average wait is the sum of squared headways over twice their sum, which is why bunching hurts riders even when the average gap is fine. In numbers: 6 buses an hour, every 10 minutes, and riders wait 5 minutes on average. The same buses in pairs, 18 then 2 minutes apart, and riders wait 8.2. Same buses, same average gap, 3.2 minutes of excess wait, and half of those buses are exactly on time. That is why the query keeps only frequent routes; infrequent routes get a timetable-adherence query instead, with early departures counted on their own. Scheduled headways come from GTFS, so a canceled trip shows up as a longer actual gap. Missed trips come from comparing scheduled trips against stop_events, then checking the dispatch log to split cancellations from buses the location system didn’t see. The scheduled side only counts days I have events for, so a week of data isn’t compared with a month of timetable. In practice I’d also split by time band: a route every 8 minutes at peak and hourly at night pools into one scheduled wait, and the < 6 filter misclassifies it.

Sequence. Before Monday I’d pull the published GTFS schedule and, if the agency publishes one, start logging its GTFS-Realtime vehicle positions, from which I can infer stop arrivals, so I have days of data even if the internal export takes weeks to approve. Monday: ride and sit with dispatch, and ask who approves the stop-events export. Tuesday: the query above, on whichever data I have. Wednesday: rank routes by ridership change and by excess wait. If the routes that lost riders are not the ones with the worst excess wait, reliability is not the main cause, and on Friday I say so instead of demoing a bunching tool. If they line up, I break the worst corridor’s bunching out by hour. Thursday: build the slice. Friday: review with dispatchers and the sponsor.

First slice. A page on my laptop next to the dispatcher’s console for one shift, covering the worst corridor. It flags pairs of buses closer than headway / 4 and suggests holding the second at the next timepoint, using the live location feed and the same headway logic. The baseline is this week’s excess wait on that corridor, from the query above. I compare shifts that used the page against shifts that didn’t. The guardrail is end-to-end run time, because holding adds minutes for the riders already on board; if run time rises by more than the wait it saves, we stop holding and try short-turning instead.

Failure modes. If many buses have no location units, I’d check whether they cluster by garage, because then the worst routes may be the unmeasured ones. Then I’d use whatever else timestamps a bus at a stop: fare-card taps, if the validators record location; passenger-counter records; GTFS-Realtime from the buses that do report; and a street supervisor doing point checks at one timepoint on the worst corridor for the week. The measure becomes a sampled estimate, and I’d say how many trips it rests on. Schedule padding can raise on-time numbers while riders wait longer.

If ridership fell and on-time performance didn’t, I’d tell the sponsor the metric isn’t measuring what riders feel: if canceled trips don’t count against it, as I’d confirm in how they compute it, missed service is invisible, and bunched buses can each be on time. Then I’d show excess wait and missed trips on the routes that lost riders.

Close. By Friday the sponsor has excess wait and missed trips for every frequent route, a straight answer on whether reliability explains the lost riders, and one dispatcher view tested on a real shift. I won’t propose a rider app until the service it predicts is one riders can rely on.