e-neko aka e-cat of IRC told me that buses come in pairs for some reason. I did not believe him. I still find it hard to believe. It looks so strange.
Figured made into a mooovie:
https://kaka.farm/pub/images/2025-12-06-buses-simulation/bus.mp4
Simulation code:
https://kaka.farm/pub/images/2025-12-06-buses-simulation/waiting-for-the-bus.scm
Jupyter notebook on:
https://kaka.farm/pub/images/2025-12-06-buses-simulation/bus.ipynb
(import
(srfi srfi-1)
(srfi srfi-9)
(ice-9 match)
)
(define *bus-capacity* 50)
(define *bus-departure-interval* 100)
(define *bus-route-length* 10000)
(define *bus-speed* 10)
(define *number-of-stations* 100)
(define *station-fill-rate* 1)
(define *time-passenger-on* 1)
(define *time-passenger-off* 1)
(define-record-type <event>
(make-event time data)
event?
(time event-time)
(data event-data))
(define-record-type <bus-departure>
(make-bus-departure)
bus-departure?)
(define-record-type <bus-at-station>
(make-bus-at-station station-number number-of-passengers)
bus-at-station?
(station-number bus-at-station-station-number)
(number-of-passengers bus-at-station-number-of-passsangers))
(define-record-type <passenger-waiting>
(make-passenger-waiting station-number)
passenger-waiting?
(station-number passenger-waiting-station-number))
(define-record-type <world>
(make-world events stations)
world?
(events world-events)
(stations world-stations))
(define (initialise-world)
(make-world '()
(make-vector *number-of-stations* 0)))
(define (sort-event-queue event-queue)
(sort event-queue
(lambda (event-a event-b)
(< (event-time event-a)
(event-time event-b)))))
(define (tick world)
(match (world-events world)
['() (make-world (list (make-event 0 (make-bus-departure))
(make-event 0 (make-passenger-waiting (random *number-of-stations*))))
(world-stations world))]
[(and events (head . rest))
(let* ([sorted-event-queue (sort-event-queue events)]
[earliest-event (car sorted-event-queue)]
[rest-of-events (cdr sorted-event-queue)])
(match earliest-event
[($ <event> time data)
(match data
[($ <passenger-waiting> station-number)
(let ([new-events (list (make-event (+ time (* (random:uniform)
*station-fill-rate*))
(make-passenger-waiting (random *number-of-stations*))))]
[stations (world-stations world)])
(vector-set! stations station-number (1+ (vector-ref stations station-number)))
(make-world (append new-events rest-of-events)
stations))]
[($ <bus-departure>)
(let ([new-events (list (make-event (+ time *bus-departure-interval*)
(make-bus-departure))
(make-event (+ time (/ *bus-route-length* *number-of-stations*))
(make-bus-at-station 1 0)))])
(make-world (append new-events rest-of-events)
(world-stations world)))]
[($ <bus-at-station> station-number number-of-passengers)
(format #t "~A,~A~%" time station-number)
(cond
[(= station-number *number-of-stations*)
(make-world rest-of-events (world-stations world))]
[else
(let* ([stations (world-stations world)]
[waiting-at-station (vector-ref stations station-number)]
[passengers-off (round (* number-of-passengers (random:uniform)))]
[number-of-passengers-after-off (- number-of-passengers passengers-off)]
[free-seats-after-off (- *bus-capacity* number-of-passengers-after-off)]
[passengers-on (min free-seats-after-off waiting-at-station)]
[number-of-passengers-after-off-on (+ number-of-passengers-after-off passengers-on)]
[time-bus-waiting-in-station (+ (* passengers-off *time-passenger-off*)
(* passengers-on *time-passenger-on*))]
[next-time (+ time
time-bus-waiting-in-station
(/ *bus-route-length* *number-of-stations*))]
[new-events (list (make-event next-time
(make-bus-at-station (1+ station-number)
number-of-passengers-after-off-on)))])
(vector-set! stations station-number (- (vector-ref stations station-number)
passengers-on))
(make-world (append new-events rest-of-events)
stations))])]
[($ <event> time 'bus-final-arrival data)
(make-world rest-of-events
(world-stations world))])]))]))
(let loop ([world (tick (make-world '() (make-vector *number-of-stations* 0)))]
[n 0])
(cond
[(= n 100000)
'()]
[else
(loop (tick world)
(1+ n))]))
Jupyter notebook source, sorta:
from matplotlib import pyplot as plt
import numpy as np
s = open('data-file.csv', 'r').read()
l = [[float(n.split(',')[0]), int(n.split(',')[1])] for n in s.split("\n")]
def f(station_number):
last = [n for n in l if n[1] == station_number]
last = [n_1[0] - n_0[0] for n_0, n_1 in zip(last, last[1:])]
fig, ax = plt.subplots(1, 1)
ax.set_title(f'bus station #{station_number:03}')
ax.set_xlabel('time difference between two consecutive buses')
ax.set_ylabel('number of busess')
ax.hist(last, bins=100)
fig.savefig(f'bus-{station_number:03}.png')
for range(1, 101):
f(n)
Bussin, no cap
Bussin cussy, Mr. Juggalo. 🤡
Thank you for creating bus movie motion picture content!
We are living in a strange world where buses come in pairs, and I need to write it into a proper web page.
I read a paper a veeerrryyy long time ago that discussed this topic. What I remember from it is that yes, this absolutely happens and no, there isn’t a very good fix. I don’t remember all of the details, but IIRC one bus gets delayed until the second bus catches up and then they are stuck together until shift changes happen.
Speculating, what I think happens is this: Imagine a simple circular bus route with evenly spaced stops and two busses on it. The busses start at opposite sides of the circle at the beginning of a shift. Each bus is going to be hit with random delays from time to time. Now, if the distribution of the delays is evenly random, then the spacing between them will be a random walk that tends towards them staying the same distance apart. But I suspect that the distribution of delays skews both both toward a dragon-king distribution and toward one bus.
A typical skewed delay could look like an unruly passenger. There aren’t too many of them, but enough that one bus will get one in a shift. The buss is first delayed when they get on, then maybe again before they get to their stop, as the driver gives them a warning, then a third time as they get off or are kicked off.
Once one bus is delayed it will have more passengers, since more time will have passed since the non-delayed bus came by, which will end up delaying it even more until the non-delayed bus catches up with it.
All of the possible fixes I can think of either cost $ or reduce service.
- Add more slack time between stops, so that most of the time a bus can wait for a bit at each stop. This could work, but reduces capacity.
- When one bus catches the other, have stop taking passengers and jump to the other schedule. If the 2nd bus passes the first it’s going to have the extra passenger issue, so the two buses will just start playing leapfrog. This doesn’t fix anything and possibly just makes it worse. If the second bus hangs back until the first is far enough ahead, it just lost capacity.
- Have a spare bus that can jump onto the first busses schedule until it can empty and catch up. That requires a spare bus and operator on standby, which is expensive.
Holy hell! Thank you! I am too tired to understand anything, but when I wake up and read it I will still be too dumb to understand anything. I will try and read it anyway. Thank you!
Thanks! I suspect this is not too difficult to Monte Carlo model, with different routes, number of busses and delay distribution curves, possible adaptations, and impact on costs.
Thank you for creating decay content!
We are just a bunch of brains going SPLAT! against the Cosmos.
Thank you for creating pedagogic content!
Wait until you see that metro trains come in pairs

