This application reproduces the Density and Transit Service poster (Paris, Île-de-France Mobilités / Overture data) for Bucharest, and extends it with live vehicle data: every STB bus, trolleybus and tram is polled every 20 seconds, stored, and rolled up into hour-of-day statistics per 200 m hexagon. The map shows built density against transit service as scheduled (timetable) and as actually run (observed), with a slider for the hour of the day.
roll-up tables in the same DB; out/hex.json, lines.json, fleet.json, summary.json, network.json, days/*.json
prep
once (docker compose run --rm prep)
Overture buildings via DuckDB over S3
buildings.parquet, built.json
web
forever
static/, data/out/
—
There is no application server: the browser only fetches pre-computed JSON. The raw position table is never touched by the UI, so the map stays fast however much history accumulates. All services share one Docker image (Dockerfile) and one volume (./data).
One JSON with the whole TPBI fleet (~1,600 records, ~1,250 fresh). Per vehicle: id, plate, route id, direction (0 = tur, 1 = retur), position + timestamp, and automatic passenger counts (on_board, in, out) on about 45 % of vehicles. Published terms: at most one request per 10 s per IP, collect through your own backend, no resale.
Live positions (SOURCE=gtfsrt)
https://gtfs.tpbi.ro/api/gtfs-rt/vehiclePositions
Official GTFS-Realtime. About 850 trip-matched vehicles, bearings, no passenger counts. Same vehicle and route ids as above.
Live positions (SOURCE=infotb)
https://info.stb.ro/api/web/v2-6 (the InfoTB web app backend)
Reverse-engineered: protobuf responses, device headers and an App-key from the public JS bundle, one request per line and direction (~400 per snapshot). Its terms forbid reuse of the data, so it is an opt-in fallback only. Implementation in app/stb_api.py, schema in app/stb.proto.
All operators (STB, Metrorex, STV, STCM, Regio Serv). Refreshed when the cached zip is a week old. Route type 11 = trolleybus, 0 = tram, 3 = bus, 1 = metro.
Metro traffic
—
No feed publishes live metro train positions. Metrorex appears only in the timetable. Observed layers are therefore surface-only; the "Count Metrorex departures" switch adds metro departures to the scheduled layers when you want the full picture rather than a fair scheduled-vs-observed comparison.
~308k footprints in the bbox. Only ~12 % carry height or num_floors, but 77 % of apartment blocks do. Missing floors are estimated from the median floors of buildings with known floors in the same footprint-size bucket (self-calibrating). Optional: set GHSL_ANBH to a GHSL average-building-height GeoTIFF to use that instead.
Basemap
OpenFreeMap dark style (OpenMapTiles / OpenStreetMap)
Free, no key.
The project was asked for with info.stb.ro in mind. The official TPBI feeds carry the very same STB vehicles (verified by matching vehicle ids across all three sources), without the terms-of-service problem and with passenger counts, so they are the default.
Collection and processing logic
1. Collector
Fetch the feed, keep records whose position timestamp is at most 120 s old and that have a route id (depot and stale vehicles are dropped). Positions outside a sanity bbox are dropped.
Look up the route in lines.json; project the point onto the GTFS shape of its direction to get distance along route and offset from route (metres, planar projection).
Compute speed from the previous sample of the same vehicle (same source timestamp base); jumps above 120 km/h are discarded.
Assign the H3 cell (resolution 9, 174 m edge, 0.105 km²).
Append one row per vehicle to positions (primary key poll timestamp + vehicle) and one row to polls; write live.json atomically.
2. Analytics, incremental step
Only polls newer than the last processed timestamp are scanned. From them it updates:
crossings: the interpolated time each vehicle passed the mid-point of its route direction (for headways).
3. Analytics, outputs
Rates are per poll (observations ÷ polls in that day-hour), so partial hours are unbiased. A day-hour counts once it has at least 5 polls. Hour-of-day values are then averaged over all covered days of each day type (weekday / Saturday / Sunday). "Within 400 m" means the hexagon plus its six neighbours (H3 k-ring 1).
Per-day files (days/YYYY-MM-DD.json) hold the same cell rates for one day; the map's Interval mode loads a date range and averages those in the browser, so you can analyse any period that has been collected.
Raw positions older than RETENTION_DAYS (60) are deleted; the roll-up tables keep the history.
Metrics and formulas
metric
definition
Built floor area
Σ footprint × floors over the buildings whose centroid falls in the hexagon; floor height 3.2 m when converting height to floors.
Scheduled departures ≤400 m
Σ over the hexagon and its 6 neighbours of GTFS departures per hour at stops in those cells, by day type. Surface and metro are kept separate.
Observed vehicles ≤400 m
Mean number of vehicles present at any instant in the ring = Σ observations ÷ polls, averaged over covered days.
Distinct vehicles / h
Count of different vehicle ids seen in the cell in that hour, ring-summed (an approximation of passages).
Bivariate class
Terciles ("thirds") of both variables computed within the map for the selected hour. both high · density ahead · transit ahead · both low. Mixed classes use the intermediate colours.
Scheduled vs observed
Same palette with scheduled departures as the first axis and observed vehicles as the second: cyan = planned more than run, magenta = run more than planned (extra vehicles, short-turns, deviations).
Speed
Mean of the sample speeds in the cell at that hour (all days). It is a commercial speed including dwell time.
Passenger load
Mean of on_board from vehicles with passenger counters.
Bunching
In one snapshot, sort the on-route vehicles of a line and direction by distance along the route; any vehicle within 300 m of the next one is "bunched". The line's share = bunched observations ÷ on-route observations, 06–22 h. Termini inflate it slightly (vehicles lay over next to each other).
Headway
Time between successive vehicles crossing the mid-point of the route direction. Per hour: mean (minutes) and coefficient of variation (CV = standard deviation ÷ mean). TCQSM bands: A ≤ 0.21 (clockwork), B ≤ 0.30, C ≤ 0.39, D ≤ 0.52, E ≤ 0.74, F above. Weekdays only, gaps 0.5–60 min.
Fleet in service
Observations per 15-minute slot ÷ polls in the slot, by mode; today versus the day-type average (or the chosen interval).
Headline stats
As on the poster: share of the densest third of hexes that are in the lowest third for transit; share of the best-served third in the lowest third for density; Gini of transit over hexes ordered by service per m² of floor area (0 = service proportional to density). Computed for weekday 08:00 and 18:00, scheduled and observed.
Transit deserts
Hexes in the densest third and the lowest transit third, sorted by floor area.
Data quality
Share of samples within 150 m of their GTFS shape (about 70 %: the rest are deadheading or carry a stale route label), share with speed, mean position age, share with passenger counts, vehicles that have not moved for 10 minutes.
Using the map
Layer chooses what the hexagons encode. Modes filters the live-vehicle layer. Hex opacity lets the basemap show through.
Time of day: drag the slider or press play. Day type: Weekday / Sat / Sun average all covered days of that type; Interval lets you pick a date range (or click a day in Data collection) and analyses only those days.
When an hour has no observations yet for the chosen day type, the density × observed layer silently shows scheduled departures and says so under the slider.
Hover a hexagon for its numbers; click a line in the rankings or pick it in the explorer to highlight its route and see its hourly profile.
Data collection shows every collected day as a row of 24 hour-cells (brighter = more polls), the database size and the age of the last poll.
EN / RO switches the language and is remembered in the browser.
Operations
docker compose up -d --build # start everything, map on http://localhost:8080
docker compose run --rm prep # (re)build data/built.json from Overture (~5 min)
docker compose logs -f collector # ~1,250 vehicles / 115 lines every 20 s
docker compose restart analytics # force an immediate analytics run
docker compose down # stop; data/ keeps everything
variable
default
meaning
SOURCE
mobi
mobi | gtfsrt | infotb
POLL_SECONDS
20
poll interval (minimum 10 for mo-bi terms)
H3_RES
9
hexagon resolution; re-run prep and delete positions.db if you change it
ANALYTICS_MINUTES
10
analytics cadence
RETENTION_DAYS
60
raw position retention
BBOX
25.95,44.33,26.26,44.56
buildings download extent (W,S,E,N)
PORT
8080
web port
Storage: about 4 million rows and 250 MB per day of raw positions at 20 s polling; roll-up tables are a few MB per day. Back up data/positions.db with sqlite3 positions.db ".backup x.db" while running (WAL mode).
Troubleshooting
Map empty, "No data yet": the first analytics run happens ~10 s after start; reload after a minute.
Observed layers never fill: check docker compose logs collector; "snapshot failed" means the feed is unreachable, "unknown routes" means the GTFS is stale (delete data/gtfs.zip).
Buildings missing (all hexes navy in built layer): run prep.
Hexes do not render but the metro lines do: the browser blocked a CDN script (deck.gl needs h3-js loaded first).
Glossary EN → RO
English
Română
note
headway
interval de succedare
STB usage: "interval de succedare de 3–4 minute"
bunching
grupare / vehicule grupate (circulație în convoi)
no established Romanian term
fleet in service
parc circulant
STB / TPBI usage
passenger load
grad de încărcare
direction 0 / 1
tur / retur
as in the STB app
transit service (coverage)
deservire cu transport public
built floor area
suprafață construită desfășurată
urbanism term (SCD)
terciles / thirds
treimi
"terțilă" is not in DEX
commercial speed
viteză comercială
speed including stops
stop / terminus
stație / capăt de linie
Densitate și transport public în București — ajutor
Aplicația reproduce pentru București afișul Density and Transit Service (Paris, date Île-de-France Mobilités / Overture) și îl extinde cu date în timp real: toate autobuzele, troleibuzele și tramvaiele STB sunt interogate la fiecare 20 de secunde, pozițiile sunt stocate și agregate pe ore ale zilei, pe hexagoane de 200 m. Harta arată densitatea construită față de deservirea cu transport public, atât cea programată (orar), cât și cea realizată (observată), cu un cursor pentru ora din zi.
tabele agregate în aceeași bază; out/hex.json, lines.json, fleet.json, summary.json, network.json, days/*.json
prep
o singură dată (docker compose run --rm prep)
clădiri Overture prin DuckDB din S3
buildings.parquet, built.json
web
permanent
static/, data/out/
—
Nu există server de aplicație: browserul descarcă doar fișiere JSON precalculate. Tabelul brut de poziții nu este atins de interfață, așa că harta rămâne rapidă oricât istoric s-ar acumula. Toate serviciile folosesc aceeași imagine Docker (Dockerfile) și același volum (./data). Diagrama arată fluxul: surse externe → collector / prep → volumul data/ → analytics → nginx.
Un singur JSON cu întregul parc TPBI (~1.600 înregistrări, ~1.250 proaspete). Per vehicul: id, număr de înmatriculare, id linie, sens (0 = tur, 1 = retur), poziție + marcă de timp și numărătoare automată de călători (on_board, in, out) pentru circa 45 % din vehicule. Condiții publicate: cel mult o cerere la 10 s per IP, colectare prin propriul backend, fără revânzare.
Poziții în timp real (SOURCE=gtfsrt)
https://gtfs.tpbi.ro/api/gtfs-rt/vehiclePositions
GTFS-Realtime oficial. Circa 850 de vehicule asociate curselor, cu direcție de deplasare (bearing), fără călători. Aceleași id-uri de vehicule și linii.
Poziții în timp real (SOURCE=infotb)
https://info.stb.ro/api/web/v2-6 (backend-ul aplicației web InfoTB)
Obținut prin inginerie inversă: răspunsuri protobuf, anteturi de dispozitiv și o cheie din pachetul JS public, o cerere per linie și sens (~400 per instantaneu). Termenii de utilizare interzic reutilizarea datelor, deci este doar o variantă de rezervă, activată explicit. Implementare în app/stb_api.py, schema în app/stb.proto.
Toți operatorii (STB, Metrorex, STV, STCM, Regio Serv). Se reîmprospătează când arhiva locală are o săptămână. Tip rută 11 = troleibuz, 0 = tramvai, 3 = autobuz, 1 = metrou.
Trafic metrou
—
Niciun flux nu publică pozițiile trenurilor de metrou în timp real. Metrorex apare doar în orar. Straturile observate sunt deci doar pentru transportul de suprafață; comutatorul „Include plecările Metrorex” adaugă metroul în straturile programate atunci când vreți imaginea completă, nu o comparație corectă programat–observat.
Clădiri
Overture Maps, tema buildings (ediția detectată automat)
~308.000 amprente la sol în caseta de delimitare. Doar ~12 % au height sau num_floors, dar 77 % dintre blocurile de locuințe au. Numărul de niveluri lipsă este estimat din mediana nivelurilor clădirilor cu date, pe clase de mărime a amprentei (autocalibrare). Opțional: GHSL_ANBH către un GeoTIFF GHSL cu înălțimea medie a clădirilor.
Hartă de fundal
OpenFreeMap, stil dark (OpenMapTiles / OpenStreetMap)
Gratuit, fără cheie.
Proiectul a fost cerut cu info.stb.ro în minte. Fluxurile oficiale TPBI conțin exact aceleași vehicule STB (verificat prin potrivirea id-urilor în toate cele trei surse), fără problema termenilor de utilizare și cu numărătoare de călători, de aceea sunt implicite.
Logica de colectare și prelucrare
1. Colectorul
Descarcă fluxul, păstrează înregistrările cu marca de timp a poziției de cel mult 120 s și cu id de linie (vehiculele din depou sau cu date vechi sunt eliminate). Pozițiile din afara unei casete de verificare sunt eliminate.
Caută linia în lines.json; proiectează punctul pe traseul GTFS al sensului respectiv pentru a obține distanța parcursă pe traseu și abaterea față de traseu (metri, proiecție plană).
Calculează viteza din eșantionul anterior al aceluiași vehicul; salturile peste 120 km/h sunt eliminate.
Atribuie celula H3 (rezoluția 9, latură 174 m, 0,105 km²).
Adaugă un rând per vehicul în positions (cheie: momentul interogării + vehicul) și un rând în polls; scrie live.json atomic.
2. Analiza, pasul incremental
Se parcurg doar interogările mai noi decât ultima prelucrată. Din ele se actualizează:
hex_hour (celulă, zi, oră, mod): observații, observații pe traseu, sume de viteze, sume de călători.
hex_hour_veh: vehicule distincte per celulă și oră.
line_hour (linie, sens, zi, oră): observații, observații grupate, sume de viteze și de încărcare.
fleet (zi, interval de 15 min, mod): observații.
crossings: momentul interpolat în care fiecare vehicul a trecut prin mijlocul traseului (pentru intervalele de succedare).
3. Analiza, rezultate
Ratele sunt per interogare (observații ÷ interogări în ziua-ora respectivă), deci orele parțiale nu introduc erori. O zi-oră contează de la 5 interogări în sus. Valorile pe ore ale zilei sunt apoi mediate pe toate zilele acoperite din fiecare tip de zi (zi lucrătoare / sâmbătă / duminică). „Pe o rază de 400 m” înseamnă hexagonul plus cei șase vecini (inel H3 k = 1).
Fișierele zilnice (days/AAAA-LL-ZZ.json) conțin aceleași rate per celulă pentru o singură zi; modul Interval din hartă încarcă un interval de date și le mediază în browser, astfel încât se poate analiza orice perioadă colectată.
Pozițiile brute mai vechi de RETENTION_DAYS (60) sunt șterse; tabelele agregate păstrează istoricul.
Indicatori și formule
indicator
definiție
Suprafață construită desfășurată
Σ amprentă la sol × număr de niveluri pentru clădirile al căror centroid cade în hexagon; înălțimea unui nivel 3,2 m la conversia din height.
Plecări programate ≤400 m
Σ, pe hexagon și cei 6 vecini, a plecărilor GTFS pe oră din stațiile aflate în acele celule, pe tip de zi. Suprafața și metroul sunt ținute separat.
Vehicule observate ≤400 m
Numărul mediu de vehicule prezente în orice moment în inel = Σ observații ÷ interogări, mediat pe zilele acoperite.
Vehicule distincte / oră
Numărul de id-uri de vehicule diferite văzute în celulă în acea oră, însumat pe inel (o aproximare a trecerilor).
Clasă bivariată
Treimi ale ambelor variabile, calculate în interiorul hărții pentru ora selectată. ambele ridicate · densitatea peste deservire · deservirea peste densitate · ambele scăzute. Clasele mixte folosesc culorile intermediare.
Programat vs observat
Aceeași paletă, cu plecările programate pe prima axă și vehiculele observate pe a doua: cyan = planificat mai mult decât realizat, magenta = realizat mai mult decât planificat (vehicule suplimentare, curse scurtate, devieri).
Viteză
Media vitezelor eșantioanelor din celulă la acea oră (toate zilele). Este o viteză comercială, include staționările.
Grad de încărcare
Media valorilor on_board de la vehiculele cu sistem de numărare a călătorilor.
Grupare (circulație în convoi)
Într-un instantaneu, vehiculele aflate pe traseu ale unei linii și ale unui sens sunt ordonate după distanța parcursă; orice vehicul aflat la sub 300 m de următorul este „grupat”. Ponderea pe linie = observații grupate ÷ observații pe traseu, între 06 și 22. Capetele de linie umflă puțin valoarea (vehiculele staționează unul lângă altul).
Interval de succedare
Timpul dintre trecerile succesive ale vehiculelor prin mijlocul traseului unui sens. Pe oră: media (minute) și coeficientul de variație (CV = abaterea standard ÷ medie). Niveluri TCQSM: A ≤ 0,21 (ca un ceas), B ≤ 0,30, C ≤ 0,39, D ≤ 0,52, E ≤ 0,74, F peste. Doar zile lucrătoare, intervale 0,5–60 min.
Parc circulant
Observații per interval de 15 minute ÷ interogări în interval, pe mod; astăzi față de media tipului de zi (sau de intervalul ales).
Cifrele „Pe scurt”
Ca pe afiș: ponderea hexagoanelor din treimea cea mai densă aflate în treimea cea mai slab deservită; ponderea celor din treimea cea mai bine deservită aflate în treimea cu densitatea cea mai mică; indicele Gini al deservirii pe hexagoane ordonate după deservirea per m² desfășurat (0 = deservire proporțională cu densitatea). Calculate pentru zi lucrătoare 08:00 și 18:00, programat și observat.
Zone dense slab deservite
Hexagoane din treimea cea mai densă și din treimea cu cea mai slabă deservire, ordonate după suprafața desfășurată.
Calitatea datelor
Ponderea eșantioanelor la sub 150 m de traseul GTFS (circa 70 %: restul sunt curse în gol spre/dinspre depou sau au o linie atribuită învechită), ponderea cu viteză, vechimea medie a poziției, ponderea cu numărătoare de călători, vehicule care nu s-au mișcat de 10 minute.
Utilizarea hărții
Strat alege ce codifică hexagoanele. Moduri filtrează stratul cu vehicule în timp real. Opacitate hexagoane lasă harta de fundal să se vadă.
Ora din zi: trageți cursorul sau porniți animația. Tipul de zi: Zi lucrătoare / Sâmbătă / Duminică mediază toate zilele acoperite de tipul respectiv; Interval permite alegerea unui interval de date (sau clic pe o zi în Colectarea datelor) și analizează doar acele zile.
Când o oră nu are încă observații pentru tipul de zi ales, stratul densitate × observat afișează plecările programate și spune acest lucru sub cursor.
Treceți cu mouse-ul peste un hexagon pentru valori; clic pe o linie în clasamente sau alegeți-o din listă pentru a-i evidenția traseul și a vedea profilul orar.
Colectarea datelor arată fiecare zi colectată ca un rând de 24 de celule-oră (mai luminos = mai multe interogări), mărimea bazei de date și vechimea ultimei interogări.
EN / RO schimbă limba; alegerea este reținută în browser.
Operare
docker compose up -d --build # pornește tot, harta la http://localhost:8080
docker compose run --rm prep # (re)construiește data/built.json din Overture (~5 min)
docker compose logs -f collector # ~1.250 vehicule / 115 linii la fiecare 20 s
docker compose restart analytics # forțează o rulare imediată a analizei
docker compose down # oprește; data/ păstrează totul
variabilă
implicit
semnificație
SOURCE
mobi
mobi | gtfsrt | infotb
POLL_SECONDS
20
intervalul de interogare (minimum 10, conform condițiilor mo-bi)
H3_RES
9
rezoluția hexagoanelor; la schimbare rulați din nou prep și ștergeți positions.db
ANALYTICS_MINUTES
10
cadența analizei
RETENTION_DAYS
60
păstrarea pozițiilor brute
BBOX
25.95,44.33,26.26,44.56
extinderea descărcării clădirilor (V,S,E,N)
PORT
8080
portul web
Stocare: circa 4 milioane de rânduri și 250 MB pe zi de poziții brute la interogare din 20 în 20 s; tabelele agregate ocupă câțiva MB pe zi. Copie de siguranță în timpul funcționării: sqlite3 positions.db ".backup x.db" (mod WAL).
Depanare
Hartă goală, „Încă nu există date”: prima rulare a analizei are loc la ~10 s după pornire; reîncărcați după un minut.
Straturile observate nu se umplu: verificați docker compose logs collector; „snapshot failed” înseamnă flux inaccesibil, „unknown routes” înseamnă GTFS învechit (ștergeți data/gtfs.zip).
Lipsesc clădirile (toate hexagoanele bleumarin în stratul construit): rulați prep.
Hexagoanele nu se desenează, dar liniile de metrou da: browserul a blocat un script din CDN (deck.gl are nevoie de h3-js încărcat înainte).
Glosar RO → EN
Română
English
notă
interval de succedare
headway
folosit de STB: „interval de succedare de 3–4 minute”