Source: https://toddschiller.com/artifacts/bluebikes-broken/index.html

[← toddschiller.com](https://toddschiller.com/)

# Bluebikes broken-station detector

Load a month of [Bluebikes](https://en.wikipedia.org/wiki/Bluebikes)
(the Boston metro bikeshare) trip data and flag stations that probably had a broken dock.
A station looks broken when no bikes were returned during an hour where the same station
typically receives several. The flag is a
[Bayesian](https://en.wikipedia.org/wiki/Bayesian_inference)
probability combining a prior on dock failures with a
[Poisson](https://en.wikipedia.org/wiki/Poisson_distribution)
model of expected returns.

## 1. Load trip data

Everything stays in your browser. No upload.

Drop one or more `YYYYMM-bluebikes-tripdata.zip` files here, or
choose files

Trip ZIPs are published at
[bluebikes.com/system-data](https://bluebikes.com/system-data)
(hosted on the `hubway-data` S3 bucket). The S3 bucket blocks browser
fetches, so files have to be downloaded manually and then dropped back in here.

## 2. Inspect

Date

Hour of day (24h)

17:00

Smoothing window

±0 (single hour, no smoothing)
±1 (3-hour pool)
±2 (5-hour pool)
±3 (7-hour pool)

Pools each cell's counts across neighboring hours. Grid shape stays 24 columns.

Prior P(broken) for any one hour

1.0%

Min expected returns (λ) to flag

0.5

Show the math

Each **(station, day-of-week, hour-of-day)** cell gets its own
**baseline rate λ**. To compute it for a query date, take returns at
the same station and same hour-of-day on every other loaded date that fell on the
same day-of-week, then average. A month of trip data typically contributes 4 or 5
dates per day-of-week; the queried date is excluded ([leave-one-out](https://en.wikipedia.org/wiki/Leave-one-out_error)) so a long outage can't drag its own baseline toward zero. If only one date of
that day-of-week is loaded there's no out-of-sample evidence and the cell is
ineligible. Returns within a single hour are modeled as a
[Poisson](https://en.wikipedia.org/wiki/Poisson_distribution)
process, so the probability of zero returns when the dock is working is
`e−λ`.

Let `π` be the prior probability that any one hour at any one station is
"broken" (default 1%, slide to taste). When the dock is broken, returns are forced
to zero. Bayes then gives:

P(broken | 0 returns) = π / (π + (1 − π) · e−λ)

Increasing the time window pools observed counts and expected counts across ±N
hours; the same formula applies to the summed `λ`. Near hour 0 or 23 the
window is clipped (no wrap), so edge hours pool fewer hours and need a
proportionally higher baseline to clear the cutoff. Cells where the summed λ is
below the cutoff are marked
**ineligible**
because you can't distinguish a broken dock from a routinely-quiet hour.

**Known gaps.**
A station broken for the entire loaded window won't appear at all (no trip records
means no coordinate and no baseline to compare against). Trip timestamps are local
time and daylight-saving isn't accounted for: spring-forward leaves one DOW-hour
cell with no observations and fall-back double-counts another, each by one date per
year.

Map (current hour)
Top suspects (whole window)
Station drill-down
