todindex 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 (33) hide show
  1. todindex-0.1.0/CHANGELOG.md +24 -0
  2. todindex-0.1.0/CITATION.cff +20 -0
  3. todindex-0.1.0/LICENSE +21 -0
  4. todindex-0.1.0/MANIFEST.in +9 -0
  5. todindex-0.1.0/PKG-INFO +250 -0
  6. todindex-0.1.0/README.md +215 -0
  7. todindex-0.1.0/examples/quickstart.py +101 -0
  8. todindex-0.1.0/pyproject.toml +110 -0
  9. todindex-0.1.0/setup.cfg +4 -0
  10. todindex-0.1.0/src/todindex/__init__.py +98 -0
  11. todindex-0.1.0/src/todindex/__main__.py +7 -0
  12. todindex-0.1.0/src/todindex/_version.py +1 -0
  13. todindex-0.1.0/src/todindex/catchment.py +226 -0
  14. todindex-0.1.0/src/todindex/cli.py +315 -0
  15. todindex-0.1.0/src/todindex/composite.py +316 -0
  16. todindex-0.1.0/src/todindex/geometry.py +348 -0
  17. todindex-0.1.0/src/todindex/indicators.py +410 -0
  18. todindex-0.1.0/src/todindex/layers.py +316 -0
  19. todindex-0.1.0/src/todindex/nodeplace.py +166 -0
  20. todindex-0.1.0/src/todindex/py.typed +0 -0
  21. todindex-0.1.0/src/todindex/report.py +193 -0
  22. todindex-0.1.0/src/todindex.egg-info/PKG-INFO +250 -0
  23. todindex-0.1.0/src/todindex.egg-info/SOURCES.txt +31 -0
  24. todindex-0.1.0/src/todindex.egg-info/dependency_links.txt +1 -0
  25. todindex-0.1.0/src/todindex.egg-info/entry_points.txt +2 -0
  26. todindex-0.1.0/src/todindex.egg-info/requires.txt +7 -0
  27. todindex-0.1.0/src/todindex.egg-info/top_level.txt +1 -0
  28. todindex-0.1.0/tests/conftest.py +46 -0
  29. todindex-0.1.0/tests/test_composite.py +179 -0
  30. todindex-0.1.0/tests/test_geometry.py +111 -0
  31. todindex-0.1.0/tests/test_indicators.py +211 -0
  32. todindex-0.1.0/tests/test_io_cli.py +233 -0
  33. todindex-0.1.0/tests/test_nodeplace.py +74 -0
@@ -0,0 +1,24 @@
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
+ - Station catchments: Euclidean buffers (400 m and 800 m by default, any radii),
10
+ user-supplied polygons (e.g. network service areas) and network catchments from a
11
+ feature-id to network-distance table.
12
+ - 5D built-environment indicators per catchment: population, job, dwelling and
13
+ activity density; land-use entropy and jobs-population balance; street length,
14
+ intersection density (degree >= 3 nodes) and pedestrian-oriented street share;
15
+ destination counts and Hansen gravity accessibility; other transit stops,
16
+ population-weighted distance to the station and distance to the nearest station.
17
+ - Exact planar clipping of streets and land-use polygons (non-convex polygons and
18
+ holes supported); local equirectangular projection for lon/lat input.
19
+ - Node-place model (Bertolini 1999): node and place indices and classification
20
+ into balanced, unsustained node, unsustained place, stress and dependency.
21
+ - Composite TOD index: min-max or z-score normalisation, cost indicators, equal,
22
+ user, AHP (eigenvector or geometric mean, consistency ratio) or PCA weights.
23
+ - GeoJSON, CSV and Markdown outputs; `todindex` CLI with `assess`, `nodeplace`,
24
+ `composite` and `ahp` commands.
@@ -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: "todindex"
9
+ version: "0.1.0"
10
+ date-released: "2026-11-09"
11
+ license: "MIT"
12
+ repository-code: "https://gitlab.com/geophilo1/todindex_sdk"
13
+ url: "https://pypi.org/project/todindex/"
14
+ abstract: "Transit-oriented development (TOD) assessment of station areas: station catchments, 3D/5D built-environment indicators (density, diversity, design, destination accessibility, distance to transit), the Bertolini node-place model and a composite TOD index with min-max or z-score normalisation and equal, AHP or PCA weights."
15
+ keywords:
16
+ - transit-oriented development
17
+ - node-place model
18
+ - built environment
19
+ - urban planning
20
+ - gis
todindex-0.1.0/LICENSE ADDED
@@ -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,9 @@
1
+ include LICENSE
2
+ include README.md
3
+ include CHANGELOG.md
4
+ include CITATION.cff
5
+ include pyproject.toml
6
+ include src/todindex/py.typed
7
+ recursive-include src/todindex *.py
8
+ recursive-include tests *.py
9
+ recursive-include examples *.py
@@ -0,0 +1,250 @@
1
+ Metadata-Version: 2.4
2
+ Name: todindex
3
+ Version: 0.1.0
4
+ Summary: Transit-oriented development (TOD) assessment of station areas: catchments, 3D/5D built-environment indicators, Bertolini node-place model and a composite TOD index with AHP or PCA weights.
5
+ Author-email: Yusuf Eminoğlu <geospacephilo@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://gitlab.com/geophilo1/todindex_sdk
8
+ Project-URL: Repository, https://gitlab.com/geophilo1/todindex_sdk
9
+ Project-URL: Issues, https://gitlab.com/geophilo1/todindex_sdk/-/work_items
10
+ Project-URL: Changelog, https://gitlab.com/geophilo1/todindex_sdk/-/blob/main/CHANGELOG.md
11
+ Keywords: transit-oriented-development,tod,node-place-model,built-environment,5d,density,land-use-mix,walkability,accessibility,ahp,pca,composite-indicator,urban-planning,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
+ Requires-Dist: numpy>=1.21
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
31
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
32
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
33
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
34
+ Dynamic: license-file
35
+
36
+ # todindex
37
+
38
+ [![PyPI version](https://img.shields.io/pypi/v/todindex.svg)](https://pypi.org/project/todindex/)
39
+ [![Python versions](https://img.shields.io/pypi/pyversions/todindex.svg)](https://pypi.org/project/todindex/)
40
+ [![pipeline](https://gitlab.com/geophilo1/todindex_sdk/badges/main/pipeline.svg)](https://gitlab.com/geophilo1/todindex_sdk/-/pipelines)
41
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
42
+
43
+ **Transit-oriented development (TOD) assessment of station areas.**
44
+
45
+ `todindex` measures how transit-oriented the areas around stations are. It covers:
46
+
47
+ - **Station catchments**: Euclidean buffers (400 m and 800 m by default), your own
48
+ polygons (for example pedestrian-network service areas from a GIS), or network
49
+ catchments built from a table of network distances.
50
+ - **Built-environment indicators** in the "3D/5D" framework (Cervero & Kockelman
51
+ 1997; Ewing & Cervero 2010): density, diversity, design, destination
52
+ accessibility and distance to transit.
53
+ - The **node-place model** (Bertolini 1999): node and place indices and the five
54
+ classes balanced, unsustained node, unsustained place, stress and dependency.
55
+ - A **composite TOD index** with min-max or z-score normalisation and equal,
56
+ user-defined, AHP (pairwise comparisons) or PCA weights.
57
+ - **Outputs** as GeoJSON, CSV and Markdown reports, plus a command-line tool.
58
+
59
+ It needs only the Python standard library and NumPy. Geometry (clipping streets
60
+ and land-use polygons to catchments) is exact planar computation in pure Python.
61
+
62
+ ```bash
63
+ pip install todindex
64
+ ```
65
+
66
+ Requires Python 3.9 or newer.
67
+
68
+ ---
69
+
70
+ ## Quick start (Python)
71
+
72
+ ```python
73
+ from todindex import (
74
+ Station, PointFeature, LineFeature, AreaFeature,
75
+ assess, node_place, composite_index, ahp_weights, pairwise_matrix,
76
+ )
77
+
78
+ stations = [
79
+ Station("Centre", 0, 0, {"departures": 600, "lines": 5}),
80
+ Station("Suburb", 3000, 0, {"departures": 180, "lines": 1}),
81
+ ]
82
+ population = [PointFeature(120, 80, weight=350), PointFeature(3100, 50, weight=90)]
83
+ jobs = [PointFeature(-60, 40, weight=900), PointFeature(2900, -100, weight=40)]
84
+ streets = [LineFeature([(-1000, 0), (0, 0), (4000, 0)]),
85
+ LineFeature([(0, -1000), (0, 0), (0, 1000)], pedestrian=True)]
86
+
87
+ rows = assess(stations, population=population, jobs=jobs, streets=streets,
88
+ radii=(400, 800))
89
+ rows[0]["activity_density_800"] # residents + jobs per hectare
90
+ rows[0]["intersection_density_400"] # intersections per km2
91
+
92
+ np_res = node_place(rows, ["departures", "lines"],
93
+ ["activity_density_800", "intersection_density_800"])
94
+ np_res.classes # e.g. ['stress', 'dependency']
95
+
96
+ tod = composite_index(rows, ["activity_density_800", "intersection_density_800"],
97
+ normalization="minmax", weights="equal")
98
+ tod.scores, tod.ranks
99
+ ```
100
+
101
+ Longitude/latitude input: pass `lonlat=True` to `assess`; coordinates are projected
102
+ with a local equirectangular projection centred on the stations.
103
+
104
+ A longer synthetic example is in [`examples/quickstart.py`](examples/quickstart.py).
105
+
106
+ ## Quick start (command line)
107
+
108
+ ```bash
109
+ # 5D indicators for 400 m and 800 m buffers, from GeoJSON layers in lon/lat
110
+ todindex assess stations.geojson --station-id code \
111
+ --population blocks.geojson --population-field pop \
112
+ --jobs firms.geojson --jobs-field employees \
113
+ --landuse parcels.geojson --landuse-field landuse \
114
+ --streets osm_streets.geojson \
115
+ --stops bus_stops.geojson \
116
+ -o indicators.csv --geojson indicators.geojson \
117
+ --catchments buffers.geojson --report report.md
118
+
119
+ # node-place model from any table (CSV or GeoJSON)
120
+ todindex nodeplace indicators.csv --node departures,lines \
121
+ --place activity_density_800,landuse_entropy_800 -o nodeplace.csv
122
+
123
+ # AHP weights and consistency ratio from a pairwise matrix ("1/3" allowed)
124
+ todindex ahp matrix.csv
125
+
126
+ # composite index with AHP weights; walk distance is a cost indicator
127
+ todindex composite indicators.csv \
128
+ --indicators activity_density_800,landuse_entropy_800,mean_distance_to_station_800 \
129
+ --cost mean_distance_to_station_800 --ahp matrix.csv -o tod.csv
130
+ ```
131
+
132
+ Use `--crs projected` when the layers are in a metric projected CRS. `todindex ahp`
133
+ exits with code 3 when the consistency ratio exceeds 0.10.
134
+
135
+ ## API overview
136
+
137
+ | Function / class | Purpose |
138
+ |---|---|
139
+ | `Station`, `PointFeature`, `LineFeature`, `AreaFeature` | Input data model (projected metres or lon/lat) |
140
+ | `stations_from_geojson`, `points_from_geojson`, `lines_from_geojson`, `areas_from_geojson` | GeoJSON loaders |
141
+ | `Catchment.buffer / .polygon / .network` | Station catchments |
142
+ | `assess(stations, population=..., jobs=..., ...)` | 5D indicators per station and catchment |
143
+ | `entropy`, `jobs_population_balance`, `street_intersections` | Individual indicator functions |
144
+ | `node_place`, `classify_node_place` | Node-place model |
145
+ | `normalize`, `ahp_weights`, `pairwise_matrix`, `pca_weights`, `composite_index` | Composite TOD index |
146
+ | `stations_geojson`, `catchments_geojson`, `write_geojson`, `write_csv`, `read_table`, `markdown_report` | Outputs |
147
+
148
+ ### Indicators produced by `assess`
149
+
150
+ Each column is suffixed with the catchment label (`_400`, `_800`, `_net800`, `_poly` ...).
151
+
152
+ | D | Column | Definition |
153
+ |---|---|---|
154
+ | Density | `population`, `jobs`, `dwellings` | Sum of point weights inside the catchment |
155
+ | | `population_density`, `job_density`, `dwelling_density` | Per hectare of catchment |
156
+ | | `activity_density` | (residents + jobs) per hectare |
157
+ | Diversity | `landuse_entropy` | `-sum p_j ln p_j / ln K`, K = all land-use categories in the study |
158
+ | | `jobs_population_balance` | `1 - abs(J - rP) / (J + rP)`, r = regional jobs per resident by default |
159
+ | Design | `street_length_km` | Street length clipped to the catchment |
160
+ | | `intersection_count`, `intersection_density` | Nodes of degree >= 3; per km² |
161
+ | | `pedestrian_street_share` | Share of street length flagged pedestrian-oriented |
162
+ | Destination accessibility | `destinations` | (Weighted) destinations inside |
163
+ | | `destination_gravity` | Hansen (1959): `sum w exp(-beta d)` (if `gravity_beta` given) |
164
+ | Distance to transit | `transit_stops` | Other stops inside the catchment |
165
+ | | `mean_distance_to_station` | Population-weighted mean distance to the station (network distance when available) |
166
+ | | `nearest_station_m` | Distance to the nearest other station |
167
+
168
+ Land use can be polygons (the clipped area is the amount) or points with a weight
169
+ such as floor area. Streets must be noded like OpenStreetMap ways (streets crossing
170
+ at grade share a vertex). By default the OSM `highway` values `pedestrian`,
171
+ `footway`, `living_street`, `path`, `steps` and `residential` count as
172
+ pedestrian-oriented; pass your own set with `pedestrian_values`.
173
+
174
+ ## Method and sources
175
+
176
+ - **Catchments.** 400 m and 800 m are the customary 5- and 10-minute walking
177
+ radii around stations (see e.g. Guerra, Cervero & Tischler 2012 on the half-mile
178
+ circle). They are defaults only. Network catchments use your distance table, so
179
+ the package does no routing itself.
180
+ - **3D/5D.** Cervero, R. & Kockelman, K. (1997). Travel demand and the 3Ds:
181
+ density, diversity, and design. *Transportation Research Part D* 2(3), 199-219.
182
+ Ewing, R. & Cervero, R. (2010). Travel and the built environment: a
183
+ meta-analysis. *Journal of the American Planning Association* 76(3), 265-294.
184
+ - **Land-use entropy.** The normalised entropy of Cervero & Kockelman (1997) and
185
+ Frank et al. (2004). It is the same index as `landmix.entropy_index` in the
186
+ companion package [`landmix`](https://pypi.org/project/landmix/), which offers
187
+ further mix measures (HHI, Simpson, Hill numbers, dissimilarity).
188
+ - **Destination accessibility.** Hansen, W. G. (1959). How accessibility shapes
189
+ land use. *Journal of the American Institute of Planners* 25(2), 73-76.
190
+ - **Node-place model.** Bertolini, L. (1999). Spatial development patterns and
191
+ public transport: the application of an analytical model in the Netherlands.
192
+ *Planning Practice & Research* 14(2), 199-210. Node and place indices are
193
+ weighted means of min-max normalised indicators, as in later quantitative
194
+ applications (e.g. Reusser et al. 2008; Vale 2015).
195
+ - **AHP.** Saaty, T. L. (1980). *The Analytic Hierarchy Process*. McGraw-Hill.
196
+ Weights from the principal eigenvector (or row geometric means),
197
+ `CI = (lambda_max - n)/(n - 1)`, `CR = CI / RI` with Saaty's random index table
198
+ (`RANDOM_INDEX`, replaceable), and CR <= 0.10 as the acceptance rule.
199
+ - **Composite index and PCA weights.** OECD & JRC (2008). *Handbook on
200
+ Constructing Composite Indicators: Methodology and User Guide*. PCA weights:
201
+ components with eigenvalue > 1, squared loadings scaled to unit sum, each
202
+ indicator weighted by its largest scaled squared loading times the component's
203
+ share of retained variance.
204
+ - **Context: ITDP TOD Standard.** The Institute for Transportation and Development
205
+ Policy publishes a TOD Standard with its own scoring for development projects.
206
+ `todindex` does not reproduce that scoring; its indicators are research
207
+ measures for comparing station areas.
208
+
209
+ ## Limitations
210
+
211
+ - Densities assign each point feature wholly to the catchment containing it
212
+ (census blocks or buildings as centroids); there is no areal interpolation of
213
+ population polygons.
214
+ - Euclidean buffers overstate walkable areas where the street network is sparse;
215
+ use network catchments (`Catchment.network` with a service-area polygon) when
216
+ this matters.
217
+ - Node-place class thresholds (`band=0.2`, `stress=0.7`, `dependency=0.3`) are
218
+ conventions of this package; Bertolini's model is diagrammatic and gives no
219
+ numeric cut-offs. Indices are relative to the stations analysed together.
220
+ - PCA weights use unrotated components (the OECD/JRC Handbook rotates with
221
+ varimax); results are the same when a single component is retained.
222
+ - The local projection for lon/lat input is meant for city-scale study areas
223
+ (error about 0.3 % at 41° N over a 28 km north-south span).
224
+ - Coincident polygon edges are handled, but self-intersecting polygons are not
225
+ supported.
226
+
227
+ ## Development
228
+
229
+ ```bash
230
+ pip install -e ".[dev]"
231
+ python -m pytest
232
+ python -m ruff check . && python -m ruff format --check src tests
233
+ python -m mypy src
234
+ ```
235
+
236
+ ## Academic citation
237
+
238
+ ```bibtex
239
+ @software{eminoglu_todindex,
240
+ author = {Eminoğlu, Yusuf},
241
+ title = {todindex},
242
+ year = {2026},
243
+ version = {0.1.0},
244
+ url = {https://gitlab.com/geophilo1/todindex_sdk}
245
+ }
246
+ ```
247
+
248
+ ## License
249
+
250
+ MIT License. Copyright (c) 2026 Yusuf Eminoğlu. See [LICENSE](LICENSE).
@@ -0,0 +1,215 @@
1
+ # todindex
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/todindex.svg)](https://pypi.org/project/todindex/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/todindex.svg)](https://pypi.org/project/todindex/)
5
+ [![pipeline](https://gitlab.com/geophilo1/todindex_sdk/badges/main/pipeline.svg)](https://gitlab.com/geophilo1/todindex_sdk/-/pipelines)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
+
8
+ **Transit-oriented development (TOD) assessment of station areas.**
9
+
10
+ `todindex` measures how transit-oriented the areas around stations are. It covers:
11
+
12
+ - **Station catchments**: Euclidean buffers (400 m and 800 m by default), your own
13
+ polygons (for example pedestrian-network service areas from a GIS), or network
14
+ catchments built from a table of network distances.
15
+ - **Built-environment indicators** in the "3D/5D" framework (Cervero & Kockelman
16
+ 1997; Ewing & Cervero 2010): density, diversity, design, destination
17
+ accessibility and distance to transit.
18
+ - The **node-place model** (Bertolini 1999): node and place indices and the five
19
+ classes balanced, unsustained node, unsustained place, stress and dependency.
20
+ - A **composite TOD index** with min-max or z-score normalisation and equal,
21
+ user-defined, AHP (pairwise comparisons) or PCA weights.
22
+ - **Outputs** as GeoJSON, CSV and Markdown reports, plus a command-line tool.
23
+
24
+ It needs only the Python standard library and NumPy. Geometry (clipping streets
25
+ and land-use polygons to catchments) is exact planar computation in pure Python.
26
+
27
+ ```bash
28
+ pip install todindex
29
+ ```
30
+
31
+ Requires Python 3.9 or newer.
32
+
33
+ ---
34
+
35
+ ## Quick start (Python)
36
+
37
+ ```python
38
+ from todindex import (
39
+ Station, PointFeature, LineFeature, AreaFeature,
40
+ assess, node_place, composite_index, ahp_weights, pairwise_matrix,
41
+ )
42
+
43
+ stations = [
44
+ Station("Centre", 0, 0, {"departures": 600, "lines": 5}),
45
+ Station("Suburb", 3000, 0, {"departures": 180, "lines": 1}),
46
+ ]
47
+ population = [PointFeature(120, 80, weight=350), PointFeature(3100, 50, weight=90)]
48
+ jobs = [PointFeature(-60, 40, weight=900), PointFeature(2900, -100, weight=40)]
49
+ streets = [LineFeature([(-1000, 0), (0, 0), (4000, 0)]),
50
+ LineFeature([(0, -1000), (0, 0), (0, 1000)], pedestrian=True)]
51
+
52
+ rows = assess(stations, population=population, jobs=jobs, streets=streets,
53
+ radii=(400, 800))
54
+ rows[0]["activity_density_800"] # residents + jobs per hectare
55
+ rows[0]["intersection_density_400"] # intersections per km2
56
+
57
+ np_res = node_place(rows, ["departures", "lines"],
58
+ ["activity_density_800", "intersection_density_800"])
59
+ np_res.classes # e.g. ['stress', 'dependency']
60
+
61
+ tod = composite_index(rows, ["activity_density_800", "intersection_density_800"],
62
+ normalization="minmax", weights="equal")
63
+ tod.scores, tod.ranks
64
+ ```
65
+
66
+ Longitude/latitude input: pass `lonlat=True` to `assess`; coordinates are projected
67
+ with a local equirectangular projection centred on the stations.
68
+
69
+ A longer synthetic example is in [`examples/quickstart.py`](examples/quickstart.py).
70
+
71
+ ## Quick start (command line)
72
+
73
+ ```bash
74
+ # 5D indicators for 400 m and 800 m buffers, from GeoJSON layers in lon/lat
75
+ todindex assess stations.geojson --station-id code \
76
+ --population blocks.geojson --population-field pop \
77
+ --jobs firms.geojson --jobs-field employees \
78
+ --landuse parcels.geojson --landuse-field landuse \
79
+ --streets osm_streets.geojson \
80
+ --stops bus_stops.geojson \
81
+ -o indicators.csv --geojson indicators.geojson \
82
+ --catchments buffers.geojson --report report.md
83
+
84
+ # node-place model from any table (CSV or GeoJSON)
85
+ todindex nodeplace indicators.csv --node departures,lines \
86
+ --place activity_density_800,landuse_entropy_800 -o nodeplace.csv
87
+
88
+ # AHP weights and consistency ratio from a pairwise matrix ("1/3" allowed)
89
+ todindex ahp matrix.csv
90
+
91
+ # composite index with AHP weights; walk distance is a cost indicator
92
+ todindex composite indicators.csv \
93
+ --indicators activity_density_800,landuse_entropy_800,mean_distance_to_station_800 \
94
+ --cost mean_distance_to_station_800 --ahp matrix.csv -o tod.csv
95
+ ```
96
+
97
+ Use `--crs projected` when the layers are in a metric projected CRS. `todindex ahp`
98
+ exits with code 3 when the consistency ratio exceeds 0.10.
99
+
100
+ ## API overview
101
+
102
+ | Function / class | Purpose |
103
+ |---|---|
104
+ | `Station`, `PointFeature`, `LineFeature`, `AreaFeature` | Input data model (projected metres or lon/lat) |
105
+ | `stations_from_geojson`, `points_from_geojson`, `lines_from_geojson`, `areas_from_geojson` | GeoJSON loaders |
106
+ | `Catchment.buffer / .polygon / .network` | Station catchments |
107
+ | `assess(stations, population=..., jobs=..., ...)` | 5D indicators per station and catchment |
108
+ | `entropy`, `jobs_population_balance`, `street_intersections` | Individual indicator functions |
109
+ | `node_place`, `classify_node_place` | Node-place model |
110
+ | `normalize`, `ahp_weights`, `pairwise_matrix`, `pca_weights`, `composite_index` | Composite TOD index |
111
+ | `stations_geojson`, `catchments_geojson`, `write_geojson`, `write_csv`, `read_table`, `markdown_report` | Outputs |
112
+
113
+ ### Indicators produced by `assess`
114
+
115
+ Each column is suffixed with the catchment label (`_400`, `_800`, `_net800`, `_poly` ...).
116
+
117
+ | D | Column | Definition |
118
+ |---|---|---|
119
+ | Density | `population`, `jobs`, `dwellings` | Sum of point weights inside the catchment |
120
+ | | `population_density`, `job_density`, `dwelling_density` | Per hectare of catchment |
121
+ | | `activity_density` | (residents + jobs) per hectare |
122
+ | Diversity | `landuse_entropy` | `-sum p_j ln p_j / ln K`, K = all land-use categories in the study |
123
+ | | `jobs_population_balance` | `1 - abs(J - rP) / (J + rP)`, r = regional jobs per resident by default |
124
+ | Design | `street_length_km` | Street length clipped to the catchment |
125
+ | | `intersection_count`, `intersection_density` | Nodes of degree >= 3; per km² |
126
+ | | `pedestrian_street_share` | Share of street length flagged pedestrian-oriented |
127
+ | Destination accessibility | `destinations` | (Weighted) destinations inside |
128
+ | | `destination_gravity` | Hansen (1959): `sum w exp(-beta d)` (if `gravity_beta` given) |
129
+ | Distance to transit | `transit_stops` | Other stops inside the catchment |
130
+ | | `mean_distance_to_station` | Population-weighted mean distance to the station (network distance when available) |
131
+ | | `nearest_station_m` | Distance to the nearest other station |
132
+
133
+ Land use can be polygons (the clipped area is the amount) or points with a weight
134
+ such as floor area. Streets must be noded like OpenStreetMap ways (streets crossing
135
+ at grade share a vertex). By default the OSM `highway` values `pedestrian`,
136
+ `footway`, `living_street`, `path`, `steps` and `residential` count as
137
+ pedestrian-oriented; pass your own set with `pedestrian_values`.
138
+
139
+ ## Method and sources
140
+
141
+ - **Catchments.** 400 m and 800 m are the customary 5- and 10-minute walking
142
+ radii around stations (see e.g. Guerra, Cervero & Tischler 2012 on the half-mile
143
+ circle). They are defaults only. Network catchments use your distance table, so
144
+ the package does no routing itself.
145
+ - **3D/5D.** Cervero, R. & Kockelman, K. (1997). Travel demand and the 3Ds:
146
+ density, diversity, and design. *Transportation Research Part D* 2(3), 199-219.
147
+ Ewing, R. & Cervero, R. (2010). Travel and the built environment: a
148
+ meta-analysis. *Journal of the American Planning Association* 76(3), 265-294.
149
+ - **Land-use entropy.** The normalised entropy of Cervero & Kockelman (1997) and
150
+ Frank et al. (2004). It is the same index as `landmix.entropy_index` in the
151
+ companion package [`landmix`](https://pypi.org/project/landmix/), which offers
152
+ further mix measures (HHI, Simpson, Hill numbers, dissimilarity).
153
+ - **Destination accessibility.** Hansen, W. G. (1959). How accessibility shapes
154
+ land use. *Journal of the American Institute of Planners* 25(2), 73-76.
155
+ - **Node-place model.** Bertolini, L. (1999). Spatial development patterns and
156
+ public transport: the application of an analytical model in the Netherlands.
157
+ *Planning Practice & Research* 14(2), 199-210. Node and place indices are
158
+ weighted means of min-max normalised indicators, as in later quantitative
159
+ applications (e.g. Reusser et al. 2008; Vale 2015).
160
+ - **AHP.** Saaty, T. L. (1980). *The Analytic Hierarchy Process*. McGraw-Hill.
161
+ Weights from the principal eigenvector (or row geometric means),
162
+ `CI = (lambda_max - n)/(n - 1)`, `CR = CI / RI` with Saaty's random index table
163
+ (`RANDOM_INDEX`, replaceable), and CR <= 0.10 as the acceptance rule.
164
+ - **Composite index and PCA weights.** OECD & JRC (2008). *Handbook on
165
+ Constructing Composite Indicators: Methodology and User Guide*. PCA weights:
166
+ components with eigenvalue > 1, squared loadings scaled to unit sum, each
167
+ indicator weighted by its largest scaled squared loading times the component's
168
+ share of retained variance.
169
+ - **Context: ITDP TOD Standard.** The Institute for Transportation and Development
170
+ Policy publishes a TOD Standard with its own scoring for development projects.
171
+ `todindex` does not reproduce that scoring; its indicators are research
172
+ measures for comparing station areas.
173
+
174
+ ## Limitations
175
+
176
+ - Densities assign each point feature wholly to the catchment containing it
177
+ (census blocks or buildings as centroids); there is no areal interpolation of
178
+ population polygons.
179
+ - Euclidean buffers overstate walkable areas where the street network is sparse;
180
+ use network catchments (`Catchment.network` with a service-area polygon) when
181
+ this matters.
182
+ - Node-place class thresholds (`band=0.2`, `stress=0.7`, `dependency=0.3`) are
183
+ conventions of this package; Bertolini's model is diagrammatic and gives no
184
+ numeric cut-offs. Indices are relative to the stations analysed together.
185
+ - PCA weights use unrotated components (the OECD/JRC Handbook rotates with
186
+ varimax); results are the same when a single component is retained.
187
+ - The local projection for lon/lat input is meant for city-scale study areas
188
+ (error about 0.3 % at 41° N over a 28 km north-south span).
189
+ - Coincident polygon edges are handled, but self-intersecting polygons are not
190
+ supported.
191
+
192
+ ## Development
193
+
194
+ ```bash
195
+ pip install -e ".[dev]"
196
+ python -m pytest
197
+ python -m ruff check . && python -m ruff format --check src tests
198
+ python -m mypy src
199
+ ```
200
+
201
+ ## Academic citation
202
+
203
+ ```bibtex
204
+ @software{eminoglu_todindex,
205
+ author = {Eminoğlu, Yusuf},
206
+ title = {todindex},
207
+ year = {2026},
208
+ version = {0.1.0},
209
+ url = {https://gitlab.com/geophilo1/todindex_sdk}
210
+ }
211
+ ```
212
+
213
+ ## License
214
+
215
+ MIT License. Copyright (c) 2026 Yusuf Eminoğlu. See [LICENSE](LICENSE).
@@ -0,0 +1,101 @@
1
+ """todindex quickstart on a synthetic corridor (projected metres, no files needed).
2
+
3
+ Run: python examples/quickstart.py
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import random
9
+
10
+ from todindex import (
11
+ AreaFeature,
12
+ LineFeature,
13
+ PointFeature,
14
+ Station,
15
+ ahp_weights,
16
+ assess,
17
+ composite_index,
18
+ markdown_report,
19
+ node_place,
20
+ pairwise_matrix,
21
+ )
22
+
23
+ rng = random.Random(42)
24
+
25
+ # Five stations along a 6 km line, from a dense centre to a park-and-ride terminus.
26
+ stations = [
27
+ Station("Centre", 0, 0, {"departures": 600, "lines": 5}),
28
+ Station("Market", 1500, 0, {"departures": 420, "lines": 3}),
29
+ Station("Campus", 3000, 0, {"departures": 300, "lines": 2}),
30
+ Station("Suburb", 4500, 0, {"departures": 180, "lines": 1}),
31
+ Station("Terminus", 6000, 0, {"departures": 160, "lines": 2}),
32
+ ]
33
+
34
+ # Residents and jobs thin out with distance from the centre.
35
+ population, jobs = [], []
36
+ for _ in range(4000):
37
+ x, y = rng.uniform(-1000, 7000), rng.uniform(-1200, 1200)
38
+ population.append(PointFeature(x, y, weight=rng.uniform(5, 40) * (1 - x / 9000)))
39
+ if rng.random() < 0.5:
40
+ jobs.append(PointFeature(x, y, weight=rng.uniform(0, 60) * (1 - x / 7500)))
41
+
42
+ # A 200 m street grid; the east-west avenue is not pedestrian-oriented.
43
+ streets = []
44
+ for gx in range(-1000, 7001, 200):
45
+ streets.append(LineFeature([(gx, y) for y in range(-1200, 1201, 200)], pedestrian=True))
46
+ for gy in range(-1200, 1201, 200):
47
+ streets.append(LineFeature([(x, gy) for x in range(-1000, 7001, 200)], pedestrian=gy != 0))
48
+
49
+ # Land use blocks (100 m squares) with categories.
50
+ uses = ["residential", "commercial", "office", "institutional"]
51
+ landuse = []
52
+ for bx in range(-1000, 7000, 100):
53
+ for by in range(-600, 600, 100):
54
+ w = [3 + bx / 1000, 2 - bx / 4000, 1.5 - bx / 5000, 0.5]
55
+ cat = rng.choices(uses, weights=[max(v, 0.1) for v in w])[0]
56
+ ring = [(bx, by), (bx + 100, by), (bx + 100, by + 100), (bx, by + 100)]
57
+ landuse.append(AreaFeature([ring], cat))
58
+
59
+ rows = assess(
60
+ stations,
61
+ population=population,
62
+ jobs=jobs,
63
+ landuse=landuse,
64
+ landuse_categories=uses,
65
+ streets=streets,
66
+ radii=(400, 800),
67
+ )
68
+
69
+ cols = [
70
+ "station_id",
71
+ "activity_density_800",
72
+ "landuse_entropy_800",
73
+ "intersection_density_800",
74
+ "pedestrian_street_share_800",
75
+ ]
76
+ print(markdown_report(rows, cols, title="5D indicators (800 m)"))
77
+
78
+ np_res = node_place(
79
+ rows,
80
+ node_indicators=["departures", "lines"],
81
+ place_indicators=["activity_density_800", "landuse_entropy_800"],
82
+ )
83
+ for sid, n, p, c in zip(np_res.station_ids, np_res.node_index, np_res.place_index, np_res.classes):
84
+ print(f"{sid:9s} node={n:.2f} place={p:.2f} -> {c}")
85
+
86
+ indicators = ["activity_density_800", "landuse_entropy_800", "intersection_density_800"]
87
+ ahp = ahp_weights(
88
+ pairwise_matrix(
89
+ indicators,
90
+ {
91
+ ("activity_density_800", "landuse_entropy_800"): 3,
92
+ ("activity_density_800", "intersection_density_800"): 2,
93
+ ("intersection_density_800", "landuse_entropy_800"): 2,
94
+ },
95
+ ),
96
+ indicators,
97
+ )
98
+ print(f"\nAHP weights {ahp.weights} CR={ahp.consistency_ratio:.3f}")
99
+ tod = composite_index(rows, indicators, weights=ahp)
100
+ for r in tod.as_rows(rows):
101
+ print(f"{r['station_id']:9s} TOD index {r['tod_index']:.3f} (rank {r['tod_index_rank']})")