transitfreq 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- transitfreq-0.1.0/CHANGELOG.md +25 -0
- transitfreq-0.1.0/CITATION.cff +20 -0
- transitfreq-0.1.0/LICENSE +21 -0
- transitfreq-0.1.0/MANIFEST.in +10 -0
- transitfreq-0.1.0/PKG-INFO +260 -0
- transitfreq-0.1.0/README.md +224 -0
- transitfreq-0.1.0/examples/quickstart.py +48 -0
- transitfreq-0.1.0/pyproject.toml +113 -0
- transitfreq-0.1.0/setup.cfg +4 -0
- transitfreq-0.1.0/src/transitfreq/__init__.py +74 -0
- transitfreq-0.1.0/src/transitfreq/__main__.py +7 -0
- transitfreq-0.1.0/src/transitfreq/_version.py +1 -0
- transitfreq-0.1.0/src/transitfreq/analysis.py +407 -0
- transitfreq-0.1.0/src/transitfreq/cli.py +191 -0
- transitfreq-0.1.0/src/transitfreq/errors.py +7 -0
- transitfreq-0.1.0/src/transitfreq/feed.py +294 -0
- transitfreq-0.1.0/src/transitfreq/geojson.py +127 -0
- transitfreq-0.1.0/src/transitfreq/headway.py +152 -0
- transitfreq-0.1.0/src/transitfreq/los.py +140 -0
- transitfreq-0.1.0/src/transitfreq/pandas_ext.py +37 -0
- transitfreq-0.1.0/src/transitfreq/py.typed +0 -0
- transitfreq-0.1.0/src/transitfreq/schedule.py +164 -0
- transitfreq-0.1.0/src/transitfreq/timeutil.py +98 -0
- transitfreq-0.1.0/src/transitfreq.egg-info/PKG-INFO +260 -0
- transitfreq-0.1.0/src/transitfreq.egg-info/SOURCES.txt +44 -0
- transitfreq-0.1.0/src/transitfreq.egg-info/dependency_links.txt +1 -0
- transitfreq-0.1.0/src/transitfreq.egg-info/entry_points.txt +2 -0
- transitfreq-0.1.0/src/transitfreq.egg-info/requires.txt +9 -0
- transitfreq-0.1.0/src/transitfreq.egg-info/top_level.txt +1 -0
- transitfreq-0.1.0/tests/conftest.py +31 -0
- transitfreq-0.1.0/tests/data/mini_gtfs/agency.txt +2 -0
- transitfreq-0.1.0/tests/data/mini_gtfs/calendar.txt +3 -0
- transitfreq-0.1.0/tests/data/mini_gtfs/calendar_dates.txt +4 -0
- transitfreq-0.1.0/tests/data/mini_gtfs/feed_info.txt +2 -0
- transitfreq-0.1.0/tests/data/mini_gtfs/frequencies.txt +3 -0
- transitfreq-0.1.0/tests/data/mini_gtfs/routes.txt +4 -0
- transitfreq-0.1.0/tests/data/mini_gtfs/shapes.txt +5 -0
- transitfreq-0.1.0/tests/data/mini_gtfs/stop_times.txt +28 -0
- transitfreq-0.1.0/tests/data/mini_gtfs/stops.txt +7 -0
- transitfreq-0.1.0/tests/data/mini_gtfs/trips.txt +11 -0
- transitfreq-0.1.0/tests/test_analysis.py +157 -0
- transitfreq-0.1.0/tests/test_doctests.py +15 -0
- transitfreq-0.1.0/tests/test_feed_schedule.py +188 -0
- transitfreq-0.1.0/tests/test_geojson_cli.py +128 -0
- transitfreq-0.1.0/tests/test_headway_los.py +153 -0
- transitfreq-0.1.0/tests/test_pandas.py +19 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-11-09
|
|
8
|
+
### Added
|
|
9
|
+
- `GTFSFeed`: zip or folder input, streaming CSV reading (UTF-8 with or without BOM),
|
|
10
|
+
feeds nested in a zip sub-folder, cached stops/routes/trips/frequencies, shapes.
|
|
11
|
+
- Service calendar resolution from `calendar.txt` and `calendar_dates.txt`.
|
|
12
|
+
- Stop events for a service day: `frequencies.txt` expansion, interpolation of untimed
|
|
13
|
+
stops, times past 24:00, optional after-midnight trips of the previous day, boarding
|
|
14
|
+
flags (last stop and `pickup_type=1` excluded).
|
|
15
|
+
- `ServiceDay`: trips per hour by route, route-direction, stop or stop-route; route and
|
|
16
|
+
stop summaries with trips/h, mean/max headway and mean gap per time window, span of
|
|
17
|
+
service, TCQSM hours of service, quality-of-service bands and frequent flags; stops
|
|
18
|
+
combined across routes and optionally grouped by parent station.
|
|
19
|
+
- Configurable `FrequentCriteria` (default 15 min or better, 07:00-19:00, every direction).
|
|
20
|
+
- TCQSM 3rd ed. (2013) frequency and hours-of-service bands (Exhibits 5-2, 5-3), optional
|
|
21
|
+
2nd ed. letter-grade tables, and custom tables.
|
|
22
|
+
- GeoJSON export of stops (Point) and routes (MultiLineString from `shapes.txt` or stop
|
|
23
|
+
sequences) with frequency attributes.
|
|
24
|
+
- CLI: `transitfreq summary|services|routes|stops|frequent|tph|geojson`.
|
|
25
|
+
- Optional pandas helpers: `departures_frame`, `stops_frame`, `routes_frame`.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
cff-version: 1.2.0
|
|
2
|
+
message: "If you use this software, please cite it as below."
|
|
3
|
+
authors:
|
|
4
|
+
- family-names: "Eminoğlu"
|
|
5
|
+
given-names: "Yusuf"
|
|
6
|
+
email: "geospacephilo@gmail.com"
|
|
7
|
+
orcid: "https://orcid.org/0009-0005-6000-2934"
|
|
8
|
+
title: "transitfreq"
|
|
9
|
+
version: "0.1.0"
|
|
10
|
+
date-released: "2026-11-09"
|
|
11
|
+
license: "MIT"
|
|
12
|
+
repository-code: "https://github.com/YusufEminoglu/transitfreq_sdk"
|
|
13
|
+
url: "https://pypi.org/project/transitfreq/"
|
|
14
|
+
abstract: "Transit service frequency analysis from GTFS Schedule feeds: service calendars, frequencies.txt expansion, trips per hour, average and maximum headways, span and hours of service, frequent network identification, TCQSM quality-of-service bands and GeoJSON export."
|
|
15
|
+
keywords:
|
|
16
|
+
- gtfs
|
|
17
|
+
- public transport
|
|
18
|
+
- headway
|
|
19
|
+
- service frequency
|
|
20
|
+
- transport planning
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yusuf Eminoğlu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
include LICENSE
|
|
2
|
+
include README.md
|
|
3
|
+
include CHANGELOG.md
|
|
4
|
+
include CITATION.cff
|
|
5
|
+
include pyproject.toml
|
|
6
|
+
include src/transitfreq/py.typed
|
|
7
|
+
recursive-include src/transitfreq *.py
|
|
8
|
+
recursive-include tests *.py
|
|
9
|
+
recursive-include tests/data *.txt
|
|
10
|
+
recursive-include examples *.py
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: transitfreq
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Transit service frequency analysis from GTFS Schedule feeds: service calendars, frequencies.txt expansion, trips per hour, headways, span of service, frequent network, TCQSM quality-of-service bands and GeoJSON export.
|
|
5
|
+
Author-email: Yusuf Eminoğlu <geospacephilo@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/YusufEminoglu/transitfreq_sdk
|
|
8
|
+
Project-URL: Repository, https://github.com/YusufEminoglu/transitfreq_sdk
|
|
9
|
+
Project-URL: Issues, https://github.com/YusufEminoglu/transitfreq_sdk/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/YusufEminoglu/transitfreq_sdk/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: gtfs,transit,public-transport,headway,frequency,frequent-network,span-of-service,tcqsm,level-of-service,transport-planning,geojson,gis
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
20
|
+
Classifier: Environment :: Console
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
22
|
+
Classifier: Intended Audience :: Science/Research
|
|
23
|
+
Classifier: Intended Audience :: Developers
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.9
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Provides-Extra: pandas
|
|
29
|
+
Requires-Dist: pandas>=1.3.0; extra == "pandas"
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
33
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
34
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# transitfreq
|
|
38
|
+
|
|
39
|
+
[](https://pypi.org/project/transitfreq/)
|
|
40
|
+
[](https://pypi.org/project/transitfreq/)
|
|
41
|
+
[](https://github.com/YusufEminoglu/transitfreq_sdk/actions/workflows/ci.yml)
|
|
42
|
+
[](https://github.com/YusufEminoglu/transitfreq_sdk/blob/main/LICENSE)
|
|
43
|
+
|
|
44
|
+
**Transit service frequency analysis from a GTFS Schedule feed.**
|
|
45
|
+
|
|
46
|
+
`transitfreq` answers the everyday service-planning questions about a GTFS feed for
|
|
47
|
+
one date: which services run, how many trips per hour each route and stop gets, what
|
|
48
|
+
the average and worst headways are in a time window, how long the span of service is,
|
|
49
|
+
which routes and stops form the *frequent network*, and how they fall into the
|
|
50
|
+
TCQSM quality-of-service bands. Results export to CSV/JSON and to GeoJSON with the
|
|
51
|
+
frequency attributes attached, ready for QGIS or a web map.
|
|
52
|
+
|
|
53
|
+
- Reads a `.zip` or an unpacked folder, streaming `stop_times.txt` and `shapes.txt` with the standard `csv` module.
|
|
54
|
+
- No runtime dependencies (standard library only); `pandas` is an optional extra.
|
|
55
|
+
- Resolves `calendar.txt` + `calendar_dates.txt`, expands `frequencies.txt`, interpolates untimed stops, handles times past `24:00:00`.
|
|
56
|
+
- Command line tool and a small, typed Python API.
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install transitfreq # stdlib only
|
|
60
|
+
pip install "transitfreq[pandas]" # + DataFrame helpers
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Requires Python 3.9 or newer.
|
|
64
|
+
|
|
65
|
+
> `transitfreq` does not validate feeds. Check a feed first with a validator such as
|
|
66
|
+
> [`gtfslint-cli`](https://pypi.org/project/gtfslint-cli/) (`pip install gtfslint-cli`)
|
|
67
|
+
> or the MobilityData GTFS Validator; broken calendars or stop times give misleading
|
|
68
|
+
> frequencies.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Quick start (command line)
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# What runs on that date?
|
|
76
|
+
transitfreq services gtfs.zip --date 2024-05-06
|
|
77
|
+
|
|
78
|
+
# Route / direction summary for the AM peak and midday, as CSV
|
|
79
|
+
transitfreq routes gtfs.zip --date 2024-05-06 \
|
|
80
|
+
--window 07:00-09:00 --window 09:00-15:00 --format csv -o routes.csv
|
|
81
|
+
|
|
82
|
+
# Combined headways at each stop (platforms grouped by parent station)
|
|
83
|
+
transitfreq stops gtfs.zip --date 2024-05-06 --by-station --format csv -o stops.csv
|
|
84
|
+
|
|
85
|
+
# Frequent network: 10 minutes or better, 06:00-21:00, every direction
|
|
86
|
+
transitfreq frequent gtfs.zip --date 2024-05-06 --threshold 10 --window 06:00-21:00
|
|
87
|
+
|
|
88
|
+
# Trips per hour per route and direction
|
|
89
|
+
transitfreq tph gtfs.zip --date 2024-05-06 --by route_direction --format csv
|
|
90
|
+
|
|
91
|
+
# GeoJSON layers with frequency attributes
|
|
92
|
+
transitfreq geojson gtfs.zip --date 2024-05-06 --stops stops.geojson --routes routes.geojson
|
|
93
|
+
|
|
94
|
+
transitfreq summary gtfs.zip --date 2024-05-06
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Common options: `--window HH:MM-HH:MM` (repeatable, default `07:00-19:00`),
|
|
98
|
+
`--threshold` (frequent headway in minutes, default 15), `--frequent-window`,
|
|
99
|
+
`--measure max|mean`, `--any-direction`, `--include-previous-day`,
|
|
100
|
+
`--format table|csv|json`, `-o FILE`. `python -m transitfreq` works too.
|
|
101
|
+
|
|
102
|
+
## Quick start (Python)
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
import transitfreq as tf
|
|
106
|
+
|
|
107
|
+
day = tf.ServiceDay("gtfs.zip", "2024-05-06")
|
|
108
|
+
print(sorted(day.service_ids))
|
|
109
|
+
|
|
110
|
+
for r in day.route_summaries(["07:00-09:00", "09:00-15:00"]):
|
|
111
|
+
am = r.windows[0]
|
|
112
|
+
print(r.route_name, r.direction_id, am.trips_per_hour, am.mean_headway_min,
|
|
113
|
+
am.max_headway_min, r.frequency_los[am.window], r.hours_of_service, r.hours_los)
|
|
114
|
+
|
|
115
|
+
for s in day.stop_summaries(by_station=True):
|
|
116
|
+
print(s.stop_name, s.route_ids, s.windows[0].mean_headway_min, s.frequent)
|
|
117
|
+
|
|
118
|
+
print(day.frequent_routes()) # <= 15 min, 07:00-19:00
|
|
119
|
+
print(day.frequent_routes(tf.FrequentCriteria(max_headway_min=10,
|
|
120
|
+
windows=("06:00-21:00",))))
|
|
121
|
+
|
|
122
|
+
tf.write_geojson(tf.stops_geojson(day), "stops.geojson")
|
|
123
|
+
tf.write_geojson(tf.routes_geojson(day), "routes.geojson")
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
A runnable example over the bundled synthetic feed is in
|
|
127
|
+
[`examples/quickstart.py`](examples/quickstart.py).
|
|
128
|
+
|
|
129
|
+
## API overview
|
|
130
|
+
|
|
131
|
+
| Object | Purpose |
|
|
132
|
+
|---|---|
|
|
133
|
+
| `GTFSFeed(path)` / `load_feed(path)` | Zip or folder; `iter_rows(file)` streams any table; cached `stops`, `routes`, `trips`, `frequencies`; `shapes(ids)` |
|
|
134
|
+
| `active_service_ids(feed, date)` | `service_id`s running on a date (`calendar` + `calendar_dates`) |
|
|
135
|
+
| `service_departures(feed, date, include_previous_day=False)` | Every stop event of the service day as `Departure(time, stop_id, trip_id, route_id, direction_id, stop_sequence, first_stop, boarding)`, with `frequencies.txt` expanded |
|
|
136
|
+
| `ServiceDay(feed, date, ...)` | The analysis object below |
|
|
137
|
+
| `.route_summaries(windows)` | `RouteSummary` per route and direction (trip start times): trips, first/last trip, span, hours of service, peak trips/h, per-window `HeadwayStats`, LOS bands, `frequent` |
|
|
138
|
+
| `.stop_summaries(windows, by_station=False, route_ids=None)` | `StopSummary` per stop or parent station, all routes combined |
|
|
139
|
+
| `.trips_per_hour(by)` | Hourly counts by `route`, `route_direction`, `stop` or `stop_route` |
|
|
140
|
+
| `.frequent_routes(criteria)`, `.frequent_stops(criteria)` | The frequent network under a `FrequentCriteria` |
|
|
141
|
+
| `.summary()` | System overview |
|
|
142
|
+
| `headway_stats(times, window)`, `span_of_service(times)`, `tcqsm_hours_of_service(times)`, `trips_per_hour(times)` | Pure functions on sorted departure times |
|
|
143
|
+
| `frequency_los(headway, table)`, `hours_of_service_los(hours, table)` | Quality-of-service bands; `TCQSM_*` (3rd ed.) or `TCQSM2_*` (2nd ed. letters) tables, or your own `FrequencyBand` / `HoursBand` list |
|
|
144
|
+
| `stops_geojson(day)`, `routes_geojson(day)`, `write_geojson(fc, path)` | GeoJSON export |
|
|
145
|
+
| `transitfreq.pandas_ext` | `departures_frame`, `stops_frame`, `routes_frame` |
|
|
146
|
+
|
|
147
|
+
Every summary has `.as_dict()` giving flat columns such as `0700-0900_trips_per_hour`,
|
|
148
|
+
`0700-0900_mean_headway_min`, `0700-0900_max_headway_min` and `0700-0900_frequency_los`.
|
|
149
|
+
|
|
150
|
+
## Method and sources
|
|
151
|
+
|
|
152
|
+
**Service calendar.** A `service_id` is active when `calendar.txt` covers the date and the
|
|
153
|
+
weekday flag is 1, then `calendar_dates.txt` adds (`exception_type=1`) or removes (`2`) it
|
|
154
|
+
for that date ([GTFS Schedule Reference](https://gtfs.org/documentation/schedule/reference/)).
|
|
155
|
+
Feeds that use only `calendar_dates.txt` work.
|
|
156
|
+
|
|
157
|
+
**Service day and times.** GTFS times count from the start of the service day and may
|
|
158
|
+
exceed 24:00:00; they are kept as seconds since the start of the service day. With
|
|
159
|
+
`include_previous_day=True` the after-midnight part of the previous day's trips is added,
|
|
160
|
+
shifted by -24 h.
|
|
161
|
+
|
|
162
|
+
**Frequency-based trips.** For each `frequencies.txt` row, trips start at
|
|
163
|
+
`start_time, start_time + headway_secs, ...` strictly before `end_time`, and the template
|
|
164
|
+
trip's stop-time offsets are replayed for each start. `exact_times=0` and `1` are expanded
|
|
165
|
+
the same way.
|
|
166
|
+
|
|
167
|
+
**Untimed stops** are interpolated linearly by stop order between the surrounding timed
|
|
168
|
+
stops.
|
|
169
|
+
|
|
170
|
+
**Departures that count.** At stops, only boarding opportunities count: the last stop of a
|
|
171
|
+
trip and stops with `pickup_type=1` are excluded. Routes are measured at trip start times
|
|
172
|
+
(the first stop of each trip), per direction.
|
|
173
|
+
|
|
174
|
+
**Headway measures** for a half-open window `[start, end)` holding `n` departures:
|
|
175
|
+
|
|
176
|
+
| Measure | Definition |
|
|
177
|
+
|---|---|
|
|
178
|
+
| trips per hour | `n / window hours` |
|
|
179
|
+
| mean headway | `window minutes / n`, i.e. 60 / vehicles per hour, the TCQSM "average headway" |
|
|
180
|
+
| mean gap | average interval between consecutive departures in the window |
|
|
181
|
+
| max headway | longest wait for a passenger arriving at any moment in the window: largest difference in `[start, t1, ..., tn, end]` |
|
|
182
|
+
|
|
183
|
+
Worked example: departures at 07:00, 07:05 and 07:30 in 07:00-08:00 give 3 trips/h, a mean
|
|
184
|
+
headway of 20 min, a mean gap of 15 min and a max headway of 30 min (07:30 to 08:00).
|
|
185
|
+
|
|
186
|
+
**Span and hours of service.** `span_hours` is last minus first departure.
|
|
187
|
+
`hours_of_service` follows TCQSM 3rd ed., Chapter 5, p. 5-7: for service at least hourly,
|
|
188
|
+
last minus first departure plus 1 h, rounded down; for hourly-or-worse service, the
|
|
189
|
+
number of departures. Days that mix both are split into runs with gaps of at most 60 min
|
|
190
|
+
and the first rule is applied to each run. The manual's examples are reproduced in the
|
|
191
|
+
tests: every 30 min from 05:30 to 20:00 gives 15 h; departures at 5:30, 6:30, 7:30, 10:00,
|
|
192
|
+
13:00, 15:30, 16:30 and 17:30 give 8 h.
|
|
193
|
+
|
|
194
|
+
**Quality-of-service bands.** The default tables come from TCRP Report 165, *Transit
|
|
195
|
+
Capacity and Quality of Service Manual*, 3rd ed. (TRB, 2013), Chapter 5:
|
|
196
|
+
|
|
197
|
+
| Exhibit 5-2: average headway | Exhibit 5-3: hours of service |
|
|
198
|
+
|---|---|
|
|
199
|
+
| `<=5 min`, `>5-10 min`, `11-15 min`, `16-30 min`, `31-59 min`, `60 min`, `>60 min` | `>18 h`, `15-18 h`, `12-14 h`, `7-11 h`, `4-6 h`, `<4 h` |
|
|
200
|
+
|
|
201
|
+
The 3rd edition attaches **no A-F letter grades** to these measures (p. 5-1), so band
|
|
202
|
+
labels are returned. For non-integer averages the ranges are closed upward (10.5 min falls
|
|
203
|
+
in `11-15 min`). For users who need letters, `TCQSM2_FREQUENCY_LOS` and `TCQSM2_HOURS_LOS`
|
|
204
|
+
give the 2nd edition (TCRP Report 100, 2003) A-F grades, but these values were **not
|
|
205
|
+
checked against the primary document** for this release. All thresholds are
|
|
206
|
+
configurable: pass any list of `FrequencyBand` / `HoursBand` as `frequency_table=` /
|
|
207
|
+
`hours_table=`.
|
|
208
|
+
|
|
209
|
+
**Frequent network.** The default `FrequentCriteria()` is *15 min or better, 07:00-19:00,
|
|
210
|
+
in every direction, measured by max headway*. This is a common planning convention used
|
|
211
|
+
in frequent network maps (J. Walker, *Human Transit*, 2012), and TCQSM Exhibit 5-2 notes
|
|
212
|
+
that 11-15 min service is "often branded as 'frequent service'". It is not a standard:
|
|
213
|
+
the threshold, windows, measure (`max` or `mean`) and direction rule can all be changed.
|
|
214
|
+
|
|
215
|
+
**GeoJSON.** Stops are Points with `StopSummary` attributes. Routes are one
|
|
216
|
+
MultiLineString per route and direction built from every distinct `shapes.txt` shape used
|
|
217
|
+
that day; trips without a `shape_id` fall back to straight segments between their stops
|
|
218
|
+
(`geometry_source` tells which). Coordinates are WGS 84 longitude/latitude (RFC 7946).
|
|
219
|
+
|
|
220
|
+
## Limitations
|
|
221
|
+
|
|
222
|
+
- Scheduled service only: no GTFS Realtime, no reliability or headway adherence measures.
|
|
223
|
+
- No feed validation (see `gtfslint-cli`), no `timezone` handling, no GTFS-Flex or `location_groups`.
|
|
224
|
+
- Route-level figures use trip start times; on branching or short-turn routes, all starts in a direction are pooled.
|
|
225
|
+
- Combined stop headways count every route equally, even when two routes leave within the same minute.
|
|
226
|
+
- Untimed stops are interpolated by stop order, not by `shape_dist_traveled`.
|
|
227
|
+
- All active stop times of the chosen date are held in memory (the whole feed is not); very large feeds need a few hundred MB for a busy weekday.
|
|
228
|
+
- `exact_times=0` frequency trips are placed exactly at `start_time + k * headway_secs`.
|
|
229
|
+
|
|
230
|
+
## Development
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
pip install -e ".[dev,pandas]"
|
|
234
|
+
python -m pytest
|
|
235
|
+
python -m ruff check . && python -m ruff format --check src tests
|
|
236
|
+
python -m mypy src
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
The tests use a small synthetic GTFS feed in `tests/data/mini_gtfs` (frequency-based and
|
|
240
|
+
scheduled trips, a holiday exception, a calendar_dates-only service, a parent station,
|
|
241
|
+
untimed stops, a trip past midnight and a shape) with hand-computed expected values.
|
|
242
|
+
|
|
243
|
+
## Academic citation
|
|
244
|
+
|
|
245
|
+
```bibtex
|
|
246
|
+
@software{eminoglu_transitfreq,
|
|
247
|
+
author = {Eminoğlu, Yusuf},
|
|
248
|
+
title = {transitfreq},
|
|
249
|
+
year = {2026},
|
|
250
|
+
version = {0.1.0},
|
|
251
|
+
url = {https://github.com/YusufEminoglu/transitfreq_sdk}
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
See also `CITATION.cff`.
|
|
256
|
+
|
|
257
|
+
## License
|
|
258
|
+
|
|
259
|
+
MIT License, copyright (c) 2026 Yusuf Eminoğlu. See [LICENSE](LICENSE).
|
|
260
|
+
Contact: geospacephilo@gmail.com
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# transitfreq
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/transitfreq/)
|
|
4
|
+
[](https://pypi.org/project/transitfreq/)
|
|
5
|
+
[](https://github.com/YusufEminoglu/transitfreq_sdk/actions/workflows/ci.yml)
|
|
6
|
+
[](https://github.com/YusufEminoglu/transitfreq_sdk/blob/main/LICENSE)
|
|
7
|
+
|
|
8
|
+
**Transit service frequency analysis from a GTFS Schedule feed.**
|
|
9
|
+
|
|
10
|
+
`transitfreq` answers the everyday service-planning questions about a GTFS feed for
|
|
11
|
+
one date: which services run, how many trips per hour each route and stop gets, what
|
|
12
|
+
the average and worst headways are in a time window, how long the span of service is,
|
|
13
|
+
which routes and stops form the *frequent network*, and how they fall into the
|
|
14
|
+
TCQSM quality-of-service bands. Results export to CSV/JSON and to GeoJSON with the
|
|
15
|
+
frequency attributes attached, ready for QGIS or a web map.
|
|
16
|
+
|
|
17
|
+
- Reads a `.zip` or an unpacked folder, streaming `stop_times.txt` and `shapes.txt` with the standard `csv` module.
|
|
18
|
+
- No runtime dependencies (standard library only); `pandas` is an optional extra.
|
|
19
|
+
- Resolves `calendar.txt` + `calendar_dates.txt`, expands `frequencies.txt`, interpolates untimed stops, handles times past `24:00:00`.
|
|
20
|
+
- Command line tool and a small, typed Python API.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install transitfreq # stdlib only
|
|
24
|
+
pip install "transitfreq[pandas]" # + DataFrame helpers
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Requires Python 3.9 or newer.
|
|
28
|
+
|
|
29
|
+
> `transitfreq` does not validate feeds. Check a feed first with a validator such as
|
|
30
|
+
> [`gtfslint-cli`](https://pypi.org/project/gtfslint-cli/) (`pip install gtfslint-cli`)
|
|
31
|
+
> or the MobilityData GTFS Validator; broken calendars or stop times give misleading
|
|
32
|
+
> frequencies.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Quick start (command line)
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# What runs on that date?
|
|
40
|
+
transitfreq services gtfs.zip --date 2024-05-06
|
|
41
|
+
|
|
42
|
+
# Route / direction summary for the AM peak and midday, as CSV
|
|
43
|
+
transitfreq routes gtfs.zip --date 2024-05-06 \
|
|
44
|
+
--window 07:00-09:00 --window 09:00-15:00 --format csv -o routes.csv
|
|
45
|
+
|
|
46
|
+
# Combined headways at each stop (platforms grouped by parent station)
|
|
47
|
+
transitfreq stops gtfs.zip --date 2024-05-06 --by-station --format csv -o stops.csv
|
|
48
|
+
|
|
49
|
+
# Frequent network: 10 minutes or better, 06:00-21:00, every direction
|
|
50
|
+
transitfreq frequent gtfs.zip --date 2024-05-06 --threshold 10 --window 06:00-21:00
|
|
51
|
+
|
|
52
|
+
# Trips per hour per route and direction
|
|
53
|
+
transitfreq tph gtfs.zip --date 2024-05-06 --by route_direction --format csv
|
|
54
|
+
|
|
55
|
+
# GeoJSON layers with frequency attributes
|
|
56
|
+
transitfreq geojson gtfs.zip --date 2024-05-06 --stops stops.geojson --routes routes.geojson
|
|
57
|
+
|
|
58
|
+
transitfreq summary gtfs.zip --date 2024-05-06
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Common options: `--window HH:MM-HH:MM` (repeatable, default `07:00-19:00`),
|
|
62
|
+
`--threshold` (frequent headway in minutes, default 15), `--frequent-window`,
|
|
63
|
+
`--measure max|mean`, `--any-direction`, `--include-previous-day`,
|
|
64
|
+
`--format table|csv|json`, `-o FILE`. `python -m transitfreq` works too.
|
|
65
|
+
|
|
66
|
+
## Quick start (Python)
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
import transitfreq as tf
|
|
70
|
+
|
|
71
|
+
day = tf.ServiceDay("gtfs.zip", "2024-05-06")
|
|
72
|
+
print(sorted(day.service_ids))
|
|
73
|
+
|
|
74
|
+
for r in day.route_summaries(["07:00-09:00", "09:00-15:00"]):
|
|
75
|
+
am = r.windows[0]
|
|
76
|
+
print(r.route_name, r.direction_id, am.trips_per_hour, am.mean_headway_min,
|
|
77
|
+
am.max_headway_min, r.frequency_los[am.window], r.hours_of_service, r.hours_los)
|
|
78
|
+
|
|
79
|
+
for s in day.stop_summaries(by_station=True):
|
|
80
|
+
print(s.stop_name, s.route_ids, s.windows[0].mean_headway_min, s.frequent)
|
|
81
|
+
|
|
82
|
+
print(day.frequent_routes()) # <= 15 min, 07:00-19:00
|
|
83
|
+
print(day.frequent_routes(tf.FrequentCriteria(max_headway_min=10,
|
|
84
|
+
windows=("06:00-21:00",))))
|
|
85
|
+
|
|
86
|
+
tf.write_geojson(tf.stops_geojson(day), "stops.geojson")
|
|
87
|
+
tf.write_geojson(tf.routes_geojson(day), "routes.geojson")
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
A runnable example over the bundled synthetic feed is in
|
|
91
|
+
[`examples/quickstart.py`](examples/quickstart.py).
|
|
92
|
+
|
|
93
|
+
## API overview
|
|
94
|
+
|
|
95
|
+
| Object | Purpose |
|
|
96
|
+
|---|---|
|
|
97
|
+
| `GTFSFeed(path)` / `load_feed(path)` | Zip or folder; `iter_rows(file)` streams any table; cached `stops`, `routes`, `trips`, `frequencies`; `shapes(ids)` |
|
|
98
|
+
| `active_service_ids(feed, date)` | `service_id`s running on a date (`calendar` + `calendar_dates`) |
|
|
99
|
+
| `service_departures(feed, date, include_previous_day=False)` | Every stop event of the service day as `Departure(time, stop_id, trip_id, route_id, direction_id, stop_sequence, first_stop, boarding)`, with `frequencies.txt` expanded |
|
|
100
|
+
| `ServiceDay(feed, date, ...)` | The analysis object below |
|
|
101
|
+
| `.route_summaries(windows)` | `RouteSummary` per route and direction (trip start times): trips, first/last trip, span, hours of service, peak trips/h, per-window `HeadwayStats`, LOS bands, `frequent` |
|
|
102
|
+
| `.stop_summaries(windows, by_station=False, route_ids=None)` | `StopSummary` per stop or parent station, all routes combined |
|
|
103
|
+
| `.trips_per_hour(by)` | Hourly counts by `route`, `route_direction`, `stop` or `stop_route` |
|
|
104
|
+
| `.frequent_routes(criteria)`, `.frequent_stops(criteria)` | The frequent network under a `FrequentCriteria` |
|
|
105
|
+
| `.summary()` | System overview |
|
|
106
|
+
| `headway_stats(times, window)`, `span_of_service(times)`, `tcqsm_hours_of_service(times)`, `trips_per_hour(times)` | Pure functions on sorted departure times |
|
|
107
|
+
| `frequency_los(headway, table)`, `hours_of_service_los(hours, table)` | Quality-of-service bands; `TCQSM_*` (3rd ed.) or `TCQSM2_*` (2nd ed. letters) tables, or your own `FrequencyBand` / `HoursBand` list |
|
|
108
|
+
| `stops_geojson(day)`, `routes_geojson(day)`, `write_geojson(fc, path)` | GeoJSON export |
|
|
109
|
+
| `transitfreq.pandas_ext` | `departures_frame`, `stops_frame`, `routes_frame` |
|
|
110
|
+
|
|
111
|
+
Every summary has `.as_dict()` giving flat columns such as `0700-0900_trips_per_hour`,
|
|
112
|
+
`0700-0900_mean_headway_min`, `0700-0900_max_headway_min` and `0700-0900_frequency_los`.
|
|
113
|
+
|
|
114
|
+
## Method and sources
|
|
115
|
+
|
|
116
|
+
**Service calendar.** A `service_id` is active when `calendar.txt` covers the date and the
|
|
117
|
+
weekday flag is 1, then `calendar_dates.txt` adds (`exception_type=1`) or removes (`2`) it
|
|
118
|
+
for that date ([GTFS Schedule Reference](https://gtfs.org/documentation/schedule/reference/)).
|
|
119
|
+
Feeds that use only `calendar_dates.txt` work.
|
|
120
|
+
|
|
121
|
+
**Service day and times.** GTFS times count from the start of the service day and may
|
|
122
|
+
exceed 24:00:00; they are kept as seconds since the start of the service day. With
|
|
123
|
+
`include_previous_day=True` the after-midnight part of the previous day's trips is added,
|
|
124
|
+
shifted by -24 h.
|
|
125
|
+
|
|
126
|
+
**Frequency-based trips.** For each `frequencies.txt` row, trips start at
|
|
127
|
+
`start_time, start_time + headway_secs, ...` strictly before `end_time`, and the template
|
|
128
|
+
trip's stop-time offsets are replayed for each start. `exact_times=0` and `1` are expanded
|
|
129
|
+
the same way.
|
|
130
|
+
|
|
131
|
+
**Untimed stops** are interpolated linearly by stop order between the surrounding timed
|
|
132
|
+
stops.
|
|
133
|
+
|
|
134
|
+
**Departures that count.** At stops, only boarding opportunities count: the last stop of a
|
|
135
|
+
trip and stops with `pickup_type=1` are excluded. Routes are measured at trip start times
|
|
136
|
+
(the first stop of each trip), per direction.
|
|
137
|
+
|
|
138
|
+
**Headway measures** for a half-open window `[start, end)` holding `n` departures:
|
|
139
|
+
|
|
140
|
+
| Measure | Definition |
|
|
141
|
+
|---|---|
|
|
142
|
+
| trips per hour | `n / window hours` |
|
|
143
|
+
| mean headway | `window minutes / n`, i.e. 60 / vehicles per hour, the TCQSM "average headway" |
|
|
144
|
+
| mean gap | average interval between consecutive departures in the window |
|
|
145
|
+
| max headway | longest wait for a passenger arriving at any moment in the window: largest difference in `[start, t1, ..., tn, end]` |
|
|
146
|
+
|
|
147
|
+
Worked example: departures at 07:00, 07:05 and 07:30 in 07:00-08:00 give 3 trips/h, a mean
|
|
148
|
+
headway of 20 min, a mean gap of 15 min and a max headway of 30 min (07:30 to 08:00).
|
|
149
|
+
|
|
150
|
+
**Span and hours of service.** `span_hours` is last minus first departure.
|
|
151
|
+
`hours_of_service` follows TCQSM 3rd ed., Chapter 5, p. 5-7: for service at least hourly,
|
|
152
|
+
last minus first departure plus 1 h, rounded down; for hourly-or-worse service, the
|
|
153
|
+
number of departures. Days that mix both are split into runs with gaps of at most 60 min
|
|
154
|
+
and the first rule is applied to each run. The manual's examples are reproduced in the
|
|
155
|
+
tests: every 30 min from 05:30 to 20:00 gives 15 h; departures at 5:30, 6:30, 7:30, 10:00,
|
|
156
|
+
13:00, 15:30, 16:30 and 17:30 give 8 h.
|
|
157
|
+
|
|
158
|
+
**Quality-of-service bands.** The default tables come from TCRP Report 165, *Transit
|
|
159
|
+
Capacity and Quality of Service Manual*, 3rd ed. (TRB, 2013), Chapter 5:
|
|
160
|
+
|
|
161
|
+
| Exhibit 5-2: average headway | Exhibit 5-3: hours of service |
|
|
162
|
+
|---|---|
|
|
163
|
+
| `<=5 min`, `>5-10 min`, `11-15 min`, `16-30 min`, `31-59 min`, `60 min`, `>60 min` | `>18 h`, `15-18 h`, `12-14 h`, `7-11 h`, `4-6 h`, `<4 h` |
|
|
164
|
+
|
|
165
|
+
The 3rd edition attaches **no A-F letter grades** to these measures (p. 5-1), so band
|
|
166
|
+
labels are returned. For non-integer averages the ranges are closed upward (10.5 min falls
|
|
167
|
+
in `11-15 min`). For users who need letters, `TCQSM2_FREQUENCY_LOS` and `TCQSM2_HOURS_LOS`
|
|
168
|
+
give the 2nd edition (TCRP Report 100, 2003) A-F grades, but these values were **not
|
|
169
|
+
checked against the primary document** for this release. All thresholds are
|
|
170
|
+
configurable: pass any list of `FrequencyBand` / `HoursBand` as `frequency_table=` /
|
|
171
|
+
`hours_table=`.
|
|
172
|
+
|
|
173
|
+
**Frequent network.** The default `FrequentCriteria()` is *15 min or better, 07:00-19:00,
|
|
174
|
+
in every direction, measured by max headway*. This is a common planning convention used
|
|
175
|
+
in frequent network maps (J. Walker, *Human Transit*, 2012), and TCQSM Exhibit 5-2 notes
|
|
176
|
+
that 11-15 min service is "often branded as 'frequent service'". It is not a standard:
|
|
177
|
+
the threshold, windows, measure (`max` or `mean`) and direction rule can all be changed.
|
|
178
|
+
|
|
179
|
+
**GeoJSON.** Stops are Points with `StopSummary` attributes. Routes are one
|
|
180
|
+
MultiLineString per route and direction built from every distinct `shapes.txt` shape used
|
|
181
|
+
that day; trips without a `shape_id` fall back to straight segments between their stops
|
|
182
|
+
(`geometry_source` tells which). Coordinates are WGS 84 longitude/latitude (RFC 7946).
|
|
183
|
+
|
|
184
|
+
## Limitations
|
|
185
|
+
|
|
186
|
+
- Scheduled service only: no GTFS Realtime, no reliability or headway adherence measures.
|
|
187
|
+
- No feed validation (see `gtfslint-cli`), no `timezone` handling, no GTFS-Flex or `location_groups`.
|
|
188
|
+
- Route-level figures use trip start times; on branching or short-turn routes, all starts in a direction are pooled.
|
|
189
|
+
- Combined stop headways count every route equally, even when two routes leave within the same minute.
|
|
190
|
+
- Untimed stops are interpolated by stop order, not by `shape_dist_traveled`.
|
|
191
|
+
- All active stop times of the chosen date are held in memory (the whole feed is not); very large feeds need a few hundred MB for a busy weekday.
|
|
192
|
+
- `exact_times=0` frequency trips are placed exactly at `start_time + k * headway_secs`.
|
|
193
|
+
|
|
194
|
+
## Development
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
pip install -e ".[dev,pandas]"
|
|
198
|
+
python -m pytest
|
|
199
|
+
python -m ruff check . && python -m ruff format --check src tests
|
|
200
|
+
python -m mypy src
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The tests use a small synthetic GTFS feed in `tests/data/mini_gtfs` (frequency-based and
|
|
204
|
+
scheduled trips, a holiday exception, a calendar_dates-only service, a parent station,
|
|
205
|
+
untimed stops, a trip past midnight and a shape) with hand-computed expected values.
|
|
206
|
+
|
|
207
|
+
## Academic citation
|
|
208
|
+
|
|
209
|
+
```bibtex
|
|
210
|
+
@software{eminoglu_transitfreq,
|
|
211
|
+
author = {Eminoğlu, Yusuf},
|
|
212
|
+
title = {transitfreq},
|
|
213
|
+
year = {2026},
|
|
214
|
+
version = {0.1.0},
|
|
215
|
+
url = {https://github.com/YusufEminoglu/transitfreq_sdk}
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
See also `CITATION.cff`.
|
|
220
|
+
|
|
221
|
+
## License
|
|
222
|
+
|
|
223
|
+
MIT License, copyright (c) 2026 Yusuf Eminoğlu. See [LICENSE](LICENSE).
|
|
224
|
+
Contact: geospacephilo@gmail.com
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Quick start: analyse the small synthetic GTFS feed shipped with the tests.
|
|
2
|
+
|
|
3
|
+
Run from the repository root:
|
|
4
|
+
|
|
5
|
+
python examples/quickstart.py [path/to/gtfs.zip] [YYYY-MM-DD]
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import transitfreq as tf
|
|
14
|
+
|
|
15
|
+
HERE = Path(__file__).resolve().parent
|
|
16
|
+
feed = sys.argv[1] if len(sys.argv) > 1 else HERE.parent / "tests" / "data" / "mini_gtfs"
|
|
17
|
+
date = sys.argv[2] if len(sys.argv) > 2 else "2024-05-06"
|
|
18
|
+
|
|
19
|
+
day = tf.ServiceDay(feed, date)
|
|
20
|
+
print(f"Service on {day.date}: {sorted(day.service_ids)}")
|
|
21
|
+
|
|
22
|
+
print("\nRoutes (trip starts), AM peak and midday:")
|
|
23
|
+
for r in day.route_summaries(["07:00-09:00", "09:00-15:00"]):
|
|
24
|
+
am, mid = r.windows
|
|
25
|
+
print(
|
|
26
|
+
f" route {r.route_name:>3} dir {r.direction_id}: {r.n_trips:3d} trips, "
|
|
27
|
+
f"AM {am.trips_per_hour:4.1f} trips/h ({r.frequency_los[am.window]}), "
|
|
28
|
+
f"midday max headway {mid.max_headway_min:5.1f} min, "
|
|
29
|
+
f"{r.hours_of_service} h of service ({r.hours_los})"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
print("\nStops, combined across routes, 07:00-19:00:")
|
|
33
|
+
for s in day.stop_summaries(by_station=True):
|
|
34
|
+
w = s.windows[0]
|
|
35
|
+
print(
|
|
36
|
+
f" {s.stop_name:<16} routes={','.join(s.route_ids):<6} "
|
|
37
|
+
f"mean headway={w.mean_headway_min or float('nan'):5.1f} min frequent={s.frequent}"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
print("\nFrequent network (<= 15 min, 07:00-19:00):", day.frequent_routes())
|
|
41
|
+
ten = tf.FrequentCriteria(max_headway_min=10, windows=("07:00-09:00", "16:00-18:00"))
|
|
42
|
+
print("Routes with <= 10 min peak service:", day.frequent_routes(ten))
|
|
43
|
+
|
|
44
|
+
out = HERE / "out"
|
|
45
|
+
out.mkdir(exist_ok=True)
|
|
46
|
+
tf.write_geojson(tf.stops_geojson(day), out / "stops.geojson")
|
|
47
|
+
tf.write_geojson(tf.routes_geojson(day), out / "routes.geojson")
|
|
48
|
+
print(f"\nGeoJSON written to {out}")
|