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.
Files changed (46) hide show
  1. transitfreq-0.1.0/CHANGELOG.md +25 -0
  2. transitfreq-0.1.0/CITATION.cff +20 -0
  3. transitfreq-0.1.0/LICENSE +21 -0
  4. transitfreq-0.1.0/MANIFEST.in +10 -0
  5. transitfreq-0.1.0/PKG-INFO +260 -0
  6. transitfreq-0.1.0/README.md +224 -0
  7. transitfreq-0.1.0/examples/quickstart.py +48 -0
  8. transitfreq-0.1.0/pyproject.toml +113 -0
  9. transitfreq-0.1.0/setup.cfg +4 -0
  10. transitfreq-0.1.0/src/transitfreq/__init__.py +74 -0
  11. transitfreq-0.1.0/src/transitfreq/__main__.py +7 -0
  12. transitfreq-0.1.0/src/transitfreq/_version.py +1 -0
  13. transitfreq-0.1.0/src/transitfreq/analysis.py +407 -0
  14. transitfreq-0.1.0/src/transitfreq/cli.py +191 -0
  15. transitfreq-0.1.0/src/transitfreq/errors.py +7 -0
  16. transitfreq-0.1.0/src/transitfreq/feed.py +294 -0
  17. transitfreq-0.1.0/src/transitfreq/geojson.py +127 -0
  18. transitfreq-0.1.0/src/transitfreq/headway.py +152 -0
  19. transitfreq-0.1.0/src/transitfreq/los.py +140 -0
  20. transitfreq-0.1.0/src/transitfreq/pandas_ext.py +37 -0
  21. transitfreq-0.1.0/src/transitfreq/py.typed +0 -0
  22. transitfreq-0.1.0/src/transitfreq/schedule.py +164 -0
  23. transitfreq-0.1.0/src/transitfreq/timeutil.py +98 -0
  24. transitfreq-0.1.0/src/transitfreq.egg-info/PKG-INFO +260 -0
  25. transitfreq-0.1.0/src/transitfreq.egg-info/SOURCES.txt +44 -0
  26. transitfreq-0.1.0/src/transitfreq.egg-info/dependency_links.txt +1 -0
  27. transitfreq-0.1.0/src/transitfreq.egg-info/entry_points.txt +2 -0
  28. transitfreq-0.1.0/src/transitfreq.egg-info/requires.txt +9 -0
  29. transitfreq-0.1.0/src/transitfreq.egg-info/top_level.txt +1 -0
  30. transitfreq-0.1.0/tests/conftest.py +31 -0
  31. transitfreq-0.1.0/tests/data/mini_gtfs/agency.txt +2 -0
  32. transitfreq-0.1.0/tests/data/mini_gtfs/calendar.txt +3 -0
  33. transitfreq-0.1.0/tests/data/mini_gtfs/calendar_dates.txt +4 -0
  34. transitfreq-0.1.0/tests/data/mini_gtfs/feed_info.txt +2 -0
  35. transitfreq-0.1.0/tests/data/mini_gtfs/frequencies.txt +3 -0
  36. transitfreq-0.1.0/tests/data/mini_gtfs/routes.txt +4 -0
  37. transitfreq-0.1.0/tests/data/mini_gtfs/shapes.txt +5 -0
  38. transitfreq-0.1.0/tests/data/mini_gtfs/stop_times.txt +28 -0
  39. transitfreq-0.1.0/tests/data/mini_gtfs/stops.txt +7 -0
  40. transitfreq-0.1.0/tests/data/mini_gtfs/trips.txt +11 -0
  41. transitfreq-0.1.0/tests/test_analysis.py +157 -0
  42. transitfreq-0.1.0/tests/test_doctests.py +15 -0
  43. transitfreq-0.1.0/tests/test_feed_schedule.py +188 -0
  44. transitfreq-0.1.0/tests/test_geojson_cli.py +128 -0
  45. transitfreq-0.1.0/tests/test_headway_los.py +153 -0
  46. 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
+ [![PyPI version](https://img.shields.io/pypi/v/transitfreq.svg)](https://pypi.org/project/transitfreq/)
40
+ [![Python versions](https://img.shields.io/pypi/pyversions/transitfreq.svg)](https://pypi.org/project/transitfreq/)
41
+ [![CI](https://github.com/YusufEminoglu/transitfreq_sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/YusufEminoglu/transitfreq_sdk/actions/workflows/ci.yml)
42
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
+ [![PyPI version](https://img.shields.io/pypi/v/transitfreq.svg)](https://pypi.org/project/transitfreq/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/transitfreq.svg)](https://pypi.org/project/transitfreq/)
5
+ [![CI](https://github.com/YusufEminoglu/transitfreq_sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/YusufEminoglu/transitfreq_sdk/actions/workflows/ci.yml)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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}")