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.
- todindex-0.1.0/CHANGELOG.md +24 -0
- todindex-0.1.0/CITATION.cff +20 -0
- todindex-0.1.0/LICENSE +21 -0
- todindex-0.1.0/MANIFEST.in +9 -0
- todindex-0.1.0/PKG-INFO +250 -0
- todindex-0.1.0/README.md +215 -0
- todindex-0.1.0/examples/quickstart.py +101 -0
- todindex-0.1.0/pyproject.toml +110 -0
- todindex-0.1.0/setup.cfg +4 -0
- todindex-0.1.0/src/todindex/__init__.py +98 -0
- todindex-0.1.0/src/todindex/__main__.py +7 -0
- todindex-0.1.0/src/todindex/_version.py +1 -0
- todindex-0.1.0/src/todindex/catchment.py +226 -0
- todindex-0.1.0/src/todindex/cli.py +315 -0
- todindex-0.1.0/src/todindex/composite.py +316 -0
- todindex-0.1.0/src/todindex/geometry.py +348 -0
- todindex-0.1.0/src/todindex/indicators.py +410 -0
- todindex-0.1.0/src/todindex/layers.py +316 -0
- todindex-0.1.0/src/todindex/nodeplace.py +166 -0
- todindex-0.1.0/src/todindex/py.typed +0 -0
- todindex-0.1.0/src/todindex/report.py +193 -0
- todindex-0.1.0/src/todindex.egg-info/PKG-INFO +250 -0
- todindex-0.1.0/src/todindex.egg-info/SOURCES.txt +31 -0
- todindex-0.1.0/src/todindex.egg-info/dependency_links.txt +1 -0
- todindex-0.1.0/src/todindex.egg-info/entry_points.txt +2 -0
- todindex-0.1.0/src/todindex.egg-info/requires.txt +7 -0
- todindex-0.1.0/src/todindex.egg-info/top_level.txt +1 -0
- todindex-0.1.0/tests/conftest.py +46 -0
- todindex-0.1.0/tests/test_composite.py +179 -0
- todindex-0.1.0/tests/test_geometry.py +111 -0
- todindex-0.1.0/tests/test_indicators.py +211 -0
- todindex-0.1.0/tests/test_io_cli.py +233 -0
- 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.
|
todindex-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+
[](https://pypi.org/project/todindex/)
|
|
39
|
+
[](https://pypi.org/project/todindex/)
|
|
40
|
+
[](https://gitlab.com/geophilo1/todindex_sdk/-/pipelines)
|
|
41
|
+
[](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).
|
todindex-0.1.0/README.md
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# todindex
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/todindex/)
|
|
4
|
+
[](https://pypi.org/project/todindex/)
|
|
5
|
+
[](https://gitlab.com/geophilo1/todindex_sdk/-/pipelines)
|
|
6
|
+
[](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']})")
|