Bucharest Transit Density — help

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.

Architecture

mo-bi.ro / gtfs.tpbi.rolive positions (JSON / GTFS-RT) gtfs.tpbi.rostatic GTFS (timetable, shapes) Overture Maps (S3)building footprints + heights collectorpoll → project → SQLite gtfs.py (in collector)lines / stops / scheduled prep (one-off)floor area per hexagon data/ volumepositions.db (WAL)lines.json, built.json analytics (every 10 min)roll-ups → data/out/*.json web (nginx)index.htmlhelp.html, /data/
servicerunsreadswrites
collectorforever, one poll every POLL_SECONDS (20)live feed, lines.jsonpositions.db (tables positions, polls), out/live.json
analyticsforever, every ANALYTICS_MINUTES (10)positions.db, built.json, scheduled.jsonroll-up tables in the same DB; out/hex.json, lines.json, fleet.json, summary.json, network.json, days/*.json
preponce (docker compose run --rm prep)Overture buildings via DuckDB over S3buildings.parquet, built.json
webforeverstatic/, 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).

Data sources and why they were chosen

whatsourcenotes
Live positions (default, SOURCE=mobi)mo-bi.ro open data — https://mo-bi.ro/python_apiOne 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/vehiclePositionsOfficial 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.
Timetable, route shapes, stops, Metrorex geometryhttps://gtfs.tpbi.ro/regional/BUCHAREST-REGION.zipAll 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.
BuildingsOverture Maps buildings theme (release auto-detected)~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.
BasemapOpenFreeMap 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

  1. 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.
  2. 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).
  3. Compute speed from the previous sample of the same vehicle (same source timestamp base); jumps above 120 km/h are discarded.
  4. Assign the H3 cell (resolution 9, 174 m edge, 0.105 km²).
  5. 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:

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

metricdefinition
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 mMean number of vehicles present at any instant in the ring = Σ observations ÷ polls, averaged over covered days.
Distinct vehicles / hCount of different vehicle ids seen in the cell in that hour, ring-summed (an approximation of passages).
Bivariate classTerciles ("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 observedSame 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).
SpeedMean of the sample speeds in the cell at that hour (all days). It is a commercial speed including dwell time.
Passenger loadMean of on_board from vehicles with passenger counters.
BunchingIn 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).
HeadwayTime 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 serviceObservations per 15-minute slot ÷ polls in the slot, by mode; today versus the day-type average (or the chosen interval).
Headline statsAs 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 desertsHexes in the densest third and the lowest transit third, sorted by floor area.
Data qualityShare 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

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
variabledefaultmeaning
SOURCEmobimobi | gtfsrt | infotb
POLL_SECONDS20poll interval (minimum 10 for mo-bi terms)
H3_RES9hexagon resolution; re-run prep and delete positions.db if you change it
ANALYTICS_MINUTES10analytics cadence
RETENTION_DAYS60raw position retention
BBOX25.95,44.33,26.26,44.56buildings download extent (W,S,E,N)
PORT8080web 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

Glossary EN → RO

EnglishRomânănote
headwayinterval de succedareSTB usage: "interval de succedare de 3–4 minute"
bunchinggrupare / vehicule grupate (circulație în convoi)no established Romanian term
fleet in serviceparc circulantSTB / TPBI usage
passenger loadgrad de încărcare
direction 0 / 1tur / returas in the STB app
transit service (coverage)deservire cu transport public
built floor areasuprafață construită desfășuratăurbanism term (SCD)
terciles / thirdstreimi"terțilă" is not in DEX
commercial speedviteză comercialăspeed including stops
stop / terminusstaț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.

Arhitectură

mo-bi.ro / gtfs.tpbi.ropoziții în timp real gtfs.tpbi.roGTFS static (orar, trasee) Overture Maps (S3)amprente și înălțimi clădiri collectorinterogare → SQLite gtfs.py (în collector)linii / stații / orar prep (o singură dată)suprafață pe hexagon volumul data/positions.db (WAL)lines.json, built.json analytics (la 10 min)agregări → out/*.json web (nginx)index.htmlhelp.html, /data/
serviciuruleazăciteștescrie
collectorpermanent, o interogare la POLL_SECONDS (20 s)fluxul în timp real, lines.jsonpositions.db (tabelele positions, polls), out/live.json
analyticspermanent, la ANALYTICS_MINUTES (10 min)positions.db, built.json, scheduled.jsontabele agregate în aceeași bază; out/hex.json, lines.json, fleet.json, summary.json, network.json, days/*.json
prepo singură dată (docker compose run --rm prep)clădiri Overture prin DuckDB din S3buildings.parquet, built.json
webpermanentstatic/, 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.

Surse de date și motivația alegerii

cesursăobservații
Poziții în timp real (implicit, SOURCE=mobi)date deschise mo-bi.ro — https://mo-bi.ro/python_apiUn 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/vehiclePositionsGTFS-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.
Orar, trasee, stații, geometria Metrorexhttps://gtfs.tpbi.ro/regional/BUCHAREST-REGION.zipToț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ădiriOverture 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 fundalOpenFreeMap, 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

  1. 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.
  2. 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ă).
  3. Calculează viteza din eșantionul anterior al aceluiași vehicul; salturile peste 120 km/h sunt eliminate.
  4. Atribuie celula H3 (rezoluția 9, latură 174 m, 0,105 km²).
  5. 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ă:

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

indicatordefiniț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 mNumă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 observatAceeaș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ărcareMedia 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 succedareTimpul 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 circulantObservaț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 deserviteHexagoane din treimea cea mai densă și din treimea cu cea mai slabă deservire, ordonate după suprafața desfășurată.
Calitatea datelorPonderea 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

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ăimplicitsemnificație
SOURCEmobimobi | gtfsrt | infotb
POLL_SECONDS20intervalul de interogare (minimum 10, conform condițiilor mo-bi)
H3_RES9rezoluția hexagoanelor; la schimbare rulați din nou prep și ștergeți positions.db
ANALYTICS_MINUTES10cadența analizei
RETENTION_DAYS60păstrarea pozițiilor brute
BBOX25.95,44.33,26.26,44.56extinderea descărcării clădirilor (V,S,E,N)
PORT8080portul 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

Glosar RO → EN

RomânăEnglishnotă
interval de succedareheadwayfolosit de STB: „interval de succedare de 3–4 minute”
grupare / vehicule grupate (circulație în convoi)bunchingfără termen consacrat în română
parc circulantfleet in serviceuzul STB / TPBI
grad de încărcarepassenger load
tur / returdirection 0 / 1ca în aplicația STB
deservire cu transport publictransit service (coverage)
suprafață construită desfășuratăbuilt floor areatermen de urbanism (SCD)
treimiterciles / thirds„terțilă” nu apare în DEX
viteză comercialăcommercial speedinclude staționările
stație / capăt de liniestop / terminus