tripdist 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 (37) hide show
  1. tripdist-0.1.0/CHANGELOG.md +31 -0
  2. tripdist-0.1.0/CITATION.cff +21 -0
  3. tripdist-0.1.0/LICENSE +21 -0
  4. tripdist-0.1.0/MANIFEST.in +9 -0
  5. tripdist-0.1.0/PKG-INFO +251 -0
  6. tripdist-0.1.0/README.md +216 -0
  7. tripdist-0.1.0/examples/quickstart.py +56 -0
  8. tripdist-0.1.0/pyproject.toml +109 -0
  9. tripdist-0.1.0/setup.cfg +4 -0
  10. tripdist-0.1.0/src/tripdist/__init__.py +131 -0
  11. tripdist-0.1.0/src/tripdist/__main__.py +7 -0
  12. tripdist-0.1.0/src/tripdist/_common.py +106 -0
  13. tripdist-0.1.0/src/tripdist/_version.py +1 -0
  14. tripdist-0.1.0/src/tripdist/balancing.py +219 -0
  15. tripdist-0.1.0/src/tripdist/calibration.py +296 -0
  16. tripdist-0.1.0/src/tripdist/cli.py +282 -0
  17. tripdist-0.1.0/src/tripdist/deterrence.py +233 -0
  18. tripdist-0.1.0/src/tripdist/diagnostics.py +138 -0
  19. tripdist-0.1.0/src/tripdist/generation.py +282 -0
  20. tripdist-0.1.0/src/tripdist/gravity.py +280 -0
  21. tripdist-0.1.0/src/tripdist/growth.py +290 -0
  22. tripdist-0.1.0/src/tripdist/io.py +208 -0
  23. tripdist-0.1.0/src/tripdist/py.typed +0 -0
  24. tripdist-0.1.0/src/tripdist/tlfd.py +178 -0
  25. tripdist-0.1.0/src/tripdist.egg-info/PKG-INFO +251 -0
  26. tripdist-0.1.0/src/tripdist.egg-info/SOURCES.txt +35 -0
  27. tripdist-0.1.0/src/tripdist.egg-info/dependency_links.txt +1 -0
  28. tripdist-0.1.0/src/tripdist.egg-info/entry_points.txt +2 -0
  29. tripdist-0.1.0/src/tripdist.egg-info/requires.txt +7 -0
  30. tripdist-0.1.0/src/tripdist.egg-info/top_level.txt +1 -0
  31. tripdist-0.1.0/tests/conftest.py +34 -0
  32. tripdist-0.1.0/tests/test_balancing_growth.py +156 -0
  33. tripdist-0.1.0/tests/test_calibration_tlfd.py +184 -0
  34. tripdist-0.1.0/tests/test_common.py +32 -0
  35. tripdist-0.1.0/tests/test_generation.py +111 -0
  36. tripdist-0.1.0/tests/test_gravity.py +177 -0
  37. tripdist-0.1.0/tests/test_io_cli.py +175 -0
@@ -0,0 +1,31 @@
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-15
8
+ ### Added
9
+ - Trip generation: OLS regression with standard errors, t statistics and R²;
10
+ cross-classification (category analysis) trip rates; production/attraction balancing
11
+ with optional fixed zones.
12
+ - Growth-factor methods: uniform, origin- and destination-specific, average factor,
13
+ Detroit, Fratar and Furness, with convergence histories.
14
+ - Furness / IPF biproportional balancing with margin-mismatch handling and diagnostics.
15
+ - Deterrence functions: exponential, power, combined (Tanner), gamma and tabulated
16
+ friction factors; JSON-style specs.
17
+ - Gravity models: unconstrained, production-, attraction- and doubly constrained, with
18
+ K-factors, intrazonal exclusion and nearest-neighbour intrazonal costs.
19
+ - Calibration: Hyman's (1969) method for any single deterrence parameter, iterative
20
+ friction-factor fitting to an observed TLFD, ratio K-factors by zone or district.
21
+ - Trip length frequency distributions, coincidence ratio, RMSE / %RMSE / R² / GEH matrix
22
+ comparison, district aggregation.
23
+ - CSV / JSON I/O and the command line: `tripdist balance | growth | gravity | calibrate | tlfd`.
24
+
25
+ ### Fixed
26
+ - Corrected section references to Ortúzar & Willumsen (2011): Hyman's calibration method
27
+ is in section 5.5 (calibration of gravity models), not 5.6; other uncertain section
28
+ numbers replaced by chapter references.
29
+ - Gamma deterrence: documented the sign mapping to the NCHRP Report 716 notation
30
+ `a t^b exp(c t)` (`b = -b_716`, `g = -c_716`); the form was previously described as the
31
+ report's own parameterisation.
@@ -0,0 +1,21 @@
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: "tripdist"
9
+ version: "0.1.0"
10
+ date-released: "2026-11-15"
11
+ license: "MIT"
12
+ repository-code: "https://gitlab.com/geophilo1/tripdist_sdk"
13
+ url: "https://pypi.org/project/tripdist/"
14
+ abstract: "Trip generation and distribution for the four-step travel demand model: regression and cross-classification trip generation, production/attraction balancing, growth-factor methods (uniform, average, Detroit, Fratar, Furness), unconstrained, singly and doubly constrained gravity models with exponential, power, combined, gamma and tabulated deterrence functions, Hyman calibration to an observed mean trip cost, trip length frequency distribution comparison, K-factors and convergence diagnostics."
15
+ keywords:
16
+ - transport planning
17
+ - travel demand
18
+ - trip distribution
19
+ - gravity model
20
+ - Furness method
21
+ - four-step model
tripdist-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/tripdist/py.typed
7
+ recursive-include src/tripdist *.py
8
+ recursive-include tests *.py
9
+ recursive-include examples *.py
@@ -0,0 +1,251 @@
1
+ Metadata-Version: 2.4
2
+ Name: tripdist
3
+ Version: 0.1.0
4
+ Summary: Trip generation and distribution for the four-step model: regression and cross-classification, P/A balancing, growth-factor methods, gravity models with Furness balancing, Hyman calibration and trip length frequency diagnostics.
5
+ Author-email: Yusuf Eminoğlu <geospacephilo@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://gitlab.com/geophilo1/tripdist_sdk
8
+ Project-URL: Repository, https://gitlab.com/geophilo1/tripdist_sdk
9
+ Project-URL: Issues, https://gitlab.com/geophilo1/tripdist_sdk/-/work_items
10
+ Project-URL: Changelog, https://gitlab.com/geophilo1/tripdist_sdk/-/blob/main/CHANGELOG.md
11
+ Keywords: transport-planning,travel-demand,four-step-model,trip-generation,trip-distribution,gravity-model,furness,fratar,ipf,hyman,origin-destination,od-matrix,urban-planning
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
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
+ # tripdist
37
+
38
+ [![PyPI](https://img.shields.io/pypi/v/tripdist.svg)](https://pypi.org/project/tripdist/)
39
+ [![Python](https://img.shields.io/pypi/pyversions/tripdist.svg)](https://pypi.org/project/tripdist/)
40
+ [![pipeline](https://gitlab.com/geophilo1/tripdist_sdk/badges/main/pipeline.svg)](https://gitlab.com/geophilo1/tripdist_sdk/-/pipelines)
41
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://gitlab.com/geophilo1/tripdist_sdk/-/blob/main/LICENSE)
42
+
43
+ **Trip generation and distribution (stages 1 and 2 of the four-step model) in plain NumPy.**
44
+
45
+ `tripdist` covers the first two stages of the classic four-step travel demand model as
46
+ set out in Ortúzar & Willumsen, *Modelling Transport*:
47
+
48
+ - **Trip generation**: zonal or household linear regression with standard errors, t
49
+ statistics and R²; cross-classification (category analysis) trip rates; balancing of
50
+ productions and attractions.
51
+ - **Growth-factor methods**: uniform, singly constrained, average factor, Detroit, Fratar
52
+ and Furness.
53
+ - **Gravity models**: unconstrained, production-constrained, attraction-constrained and
54
+ doubly constrained (Furness / IPF balancing), with exponential, power, combined (Tanner),
55
+ gamma and tabulated deterrence functions, K-factors and intrazonal handling.
56
+ - **Calibration**: Hyman's (1969) method for a deterrence parameter against an observed
57
+ mean trip cost, iterative fitting of stepwise friction factors to an observed trip
58
+ length frequency distribution, ratio K-factors at zone or district level.
59
+ - **Diagnostics**: Furness convergence history, trip length frequency distributions and
60
+ the coincidence ratio, RMSE / %RMSE / R² / GEH matrix comparison, district aggregation.
61
+
62
+ It needs only NumPy. A command-line tool reads CSV matrices and zone tables and writes JSON
63
+ and CSV.
64
+
65
+ ## Installation
66
+
67
+ ```bash
68
+ pip install tripdist
69
+ ```
70
+
71
+ Requires Python 3.9 or newer.
72
+
73
+ ## Quickstart
74
+
75
+ ```python
76
+ import numpy as np
77
+ import tripdist as td
78
+
79
+ cost = np.array([[3, 11, 18, 22],
80
+ [12, 3, 13, 19],
81
+ [15.5, 13, 5, 7],
82
+ [24, 18, 8, 5]], dtype=float) # minutes
83
+ productions = np.array([98, 106, 122, 174], dtype=float)
84
+ attractions = np.array([102, 118, 141, 139], dtype=float)
85
+
86
+ # doubly constrained gravity model with an exponential deterrence function
87
+ res = td.gravity(cost, productions, attractions, td.exponential(0.1))
88
+ res.matrix, res.iterations, res.converged, res.mean_cost
89
+ res.history # largest relative margin error per Furness pass
90
+
91
+ # calibrate beta so the model reproduces an observed mean trip cost (Hyman 1969)
92
+ cal = td.hyman(cost, productions, attractions, target_mean_cost=10.0)
93
+ cal.value, cal.modelled_mean_cost, cal.trace
94
+
95
+ # compare trip length distributions with an observed matrix
96
+ edges = td.cost_bins(cost, 5.0)
97
+ cmp = td.compare_tlfd(cal.model.matrix, observed_matrix, cost, edges)
98
+ cmp.coincidence_ratio
99
+ ```
100
+
101
+ Trip generation and growth factors:
102
+
103
+ ```python
104
+ m = td.fit_regression(trips, np.column_stack([households, cars]), ["households", "cars"])
105
+ m.coefficients, m.t_stats, m.r_squared
106
+ rates = td.cross_classification_rates(hh_trips, list(zip(car_ownership, hh_size)))
107
+ p = td.apply_category_rates(households_by_category, rates, category_keys)
108
+ p, a = td.balance_pa(p, a, method="productions")
109
+
110
+ td.furness_growth(base, origin_totals=o_future, dest_totals=d_future)
111
+ td.fratar(base, origin_factors=tau, dest_factors=tau)
112
+ td.growth_factor(base, "detroit", origin_totals=o_future, dest_totals=d_future)
113
+ ```
114
+
115
+ Other deterrence functions and options:
116
+
117
+ ```python
118
+ td.power(2.0) # c^-n
119
+ td.combined(-0.5, 0.05) # Tanner: c^n exp(-beta c)
120
+ td.gamma(a=1.0, b=0.5, g=0.05) # a c^-b exp(-g c); parameters are yours
121
+ td.tabular([0, 5, 10, 20], [1.0, 0.6, 0.2])
122
+ td.gravity(cost, p, a, {"form": "exponential", "beta": 0.08},
123
+ constraint="production", k_factors=k, intrazonal="exclude")
124
+ td.intrazonal_costs(cost, factor=0.5, neighbours=1) # factor is a modelling choice
125
+ ```
126
+
127
+ ## Command line
128
+
129
+ ```bash
130
+ tripdist balance --zones zones.csv --method productions --csv balanced.csv
131
+ tripdist growth base.csv --method furness --targets targets.csv --matrix-out future.csv
132
+ tripdist gravity cost.csv --zones zones.csv --form exponential --beta 0.1 \
133
+ --observed observed.csv --bin-width 5 --matrix-out modelled.csv -o report.json
134
+ tripdist calibrate cost.csv --zones zones.csv --observed observed.csv
135
+ tripdist calibrate cost.csv --zones zones.csv --target-mean 14 --form power --n 1 \
136
+ --parameter n --start 1 --zero-cost-value 1
137
+ tripdist tlfd modelled.csv cost.csv --edges 0,5,10,20,40 --observed observed.csv --csv tlfd.csv
138
+ ```
139
+
140
+ Matrices are CSV grids, either plain or with a header row and a first column of zone
141
+ labels (give the top-left cell a name such as `zone`); empty cells are 0 and `inf` marks
142
+ impossible movements. Zone tables are CSV files with a header (`zone,productions,
143
+ attractions` by default; growth targets use `origins,destinations`). Deterrence functions
144
+ come from `--form` with its parameters or from a JSON file such as
145
+ `{"form": "gamma", "a": 1, "b": 0.5, "g": 0.05}`. Every command prints JSON or writes it
146
+ with `-o`.
147
+
148
+ ## API overview
149
+
150
+ | Area | Functions / classes |
151
+ | --- | --- |
152
+ | Generation | `fit_regression -> RegressionModel`, `cross_classification_rates -> CategoryRates`, `apply_category_rates`, `balance_pa` |
153
+ | Balancing | `furness -> BalanceResult`, `margin_error` |
154
+ | Growth factors | `uniform_growth`, `singly_constrained_growth`, `average_factor`, `detroit`, `fratar`, `furness_growth`, `growth_factor -> GrowthResult` |
155
+ | Deterrence | `exponential`, `power`, `combined` / `tanner`, `gamma`, `tabular`, `deterrence_from_dict -> Deterrence` |
156
+ | Gravity | `gravity -> GravityResult`, `mean_cost`, `intrazonal_costs`, `intrazonal_share` |
157
+ | Calibration | `hyman -> CalibrationResult`, `observed_mean_cost`, `calibrate_friction_factors -> FrictionCalibration`, `k_factors` |
158
+ | TLFD | `tlfd -> TLFD`, `compare_tlfd -> TLFDComparison`, `coincidence_ratio`, `cost_bins` |
159
+ | Diagnostics | `summarize`, `compare_matrices -> MatrixFit`, `geh`, `aggregate` |
160
+ | I/O | `read_matrix`, `write_matrix`, `read_zone_table`, `read_long`, `read_json`, `write_json` |
161
+
162
+ All result objects have `to_dict()` returning JSON-safe values (NaN and infinity become
163
+ `null`).
164
+
165
+ ## Methods and sources
166
+
167
+ | Method | Definition | Source |
168
+ | --- | --- | --- |
169
+ | Regression trip generation | OLS `y = b0 + sum b_k x_k`, with SE, t, R², adjusted R² | Ortúzar & Willumsen (2011), ch. 4 |
170
+ | Cross-classification | mean trips per household by category; `P_i = sum_c H_ic t_c` | Wootton & Pick (1967) |
171
+ | P/A balancing | scale attractions to productions (or the reverse, or the mean); optional fixed zones | Ortúzar & Willumsen (2011), ch. 4 |
172
+ | Uniform, singly constrained | `T = tau t`; `T_ij = tau_i t_ij` | Ortúzar & Willumsen (2011), §5.2 |
173
+ | Average factor | `T_ij = t_ij (tau_i + tau_j) / 2`, iterated | Ortúzar & Willumsen (2011), §5.2 |
174
+ | Detroit | `T_ij = t_ij tau_i tau_j / tau`, iterated | Detroit Metropolitan Area Traffic Study (1956), via Ortúzar & Willumsen |
175
+ | Fratar | `T_ij = t_ij tau_i tau_j (L_i + L_j) / 2`, `L_i = sum_j t_ij / sum_j t_ij tau_j`, iterated | Fratar (1954) |
176
+ | Furness / IPF | `T_ij = A_i B_j t_ij`, alternate row and column scaling | Furness (1965); Deming & Stephan (1940); Bacharach (1970) |
177
+ | Gravity model | `T_ij = A_i O_i B_j D_j f(c_ij)` and its singly / unconstrained forms | Wilson (1970); Ortúzar & Willumsen (2011), §5.3 |
178
+ | Deterrence | `exp(-beta c)`, `c^-n`, `c^n exp(-beta c)` | Ortúzar & Willumsen (2011), ch. 5; Tanner (1961) |
179
+ | Gamma deterrence | `a c^-b exp(-g c)`, i.e. NCHRP 716's `a t^b exp(c t)` with `b = -b_716`, `g = -c_716` (form only; no parameter tables included) | NCHRP Report 716 (2012) |
180
+ | Hyman calibration | `beta_0 = 1/c*`, `beta_1 = beta_0 c_0 / c*`, then secant updates on the mean cost | Hyman (1969); Ortúzar & Willumsen (2011), §5.5 |
181
+ | Friction-factor fitting | `F_k <- F_k p_k(obs) / p_k(mod)` per cost band | FHWA (1965) |
182
+ | K-factors | ratio `observed / modelled`, by zone or district pair | FHWA (1965) (simple ratio form) |
183
+ | Coincidence ratio | `sum min(p_k, q_k) / sum max(p_k, q_k)` | FHWA Travel Model Validation Manual (2010) |
184
+ | GEH | `sqrt(2 (M - O)^2 / (M + O))` | UK DfT TAG unit M3.1 |
185
+
186
+ Notes:
187
+
188
+ - The Furness iteration stops when the largest relative margin error falls below `tol`
189
+ (default `1e-6`). Unequal target totals raise an error unless `total_mismatch` says how
190
+ to reconcile them. Seed rows or columns that are all zero but have positive targets
191
+ raise an error; other infeasible zero patterns are reported as `converged=False`.
192
+ - Infinite costs give `f = 0`. Power-type deterrence at `c = 0` raises an error unless
193
+ intrazonal costs are set or `zero_cost_value` is given.
194
+ - In Hyman's method the start value `1 / c*` applies to the exponential `beta`; for other
195
+ forms give `start`. If a secant step would make a positive parameter non-positive, the
196
+ step is halved towards zero instead.
197
+ - `intrazonal_costs` replaces the diagonal by a fraction of the cost to the nearest
198
+ zone(s); the fraction is yours to choose.
199
+
200
+ ## Limitations
201
+
202
+ - No default parameter values: published parameter sets (for example the NCHRP 716 gamma
203
+ tables) are not bundled; calibrate or supply your own.
204
+ - No acceptance thresholds for coincidence ratios, %RMSE or GEH are built in; they differ
205
+ between guidance documents.
206
+ - Dense matrices only; large zone systems (tens of thousands of zones) need care with
207
+ memory.
208
+ - No mode choice or assignment: this package covers stages 1 and 2 only.
209
+
210
+ ## References
211
+
212
+ - Bacharach, M. (1970). *Biproportional Matrices and Input-Output Change*. Cambridge
213
+ University Press.
214
+ - Cambridge Systematics (2010). *Travel Model Validation and Reasonableness Checking
215
+ Manual*, 2nd ed. Federal Highway Administration, Washington, DC.
216
+ - Cambridge Systematics et al. (2012). *NCHRP Report 716: Travel Demand Forecasting:
217
+ Parameters and Techniques*. Transportation Research Board, Washington, DC.
218
+ - Deming, W. E. & Stephan, F. F. (1940). On a least squares adjustment of a sampled
219
+ frequency table when the expected marginal totals are known. *Annals of Mathematical
220
+ Statistics* 11(4), 427-444.
221
+ - Federal Highway Administration (1965). *Calibrating and Testing a Gravity Model for Any
222
+ Size Urban Area*. US Department of Commerce, Bureau of Public Roads.
223
+ - Fratar, T. J. (1954). Vehicular trip distribution by successive approximations.
224
+ *Traffic Quarterly* 8(1), 53-65.
225
+ - Furness, K. P. (1965). Time function iteration. *Traffic Engineering and Control* 7(7),
226
+ 458-460.
227
+ - Hyman, G. M. (1969). The calibration of trip distribution models. *Environment and
228
+ Planning* 1(1), 105-112.
229
+ - Ortúzar, J. de D. & Willumsen, L. G. (2011). *Modelling Transport*, 4th ed. Wiley,
230
+ Chichester.
231
+ - Tanner, J. C. (1961). *Factors affecting the amount of travel*. Road Research Technical
232
+ Paper 51, HMSO, London.
233
+ - Wilson, A. G. (1970). *Entropy in Urban and Regional Modelling*. Pion, London.
234
+ - Wootton, H. J. & Pick, G. W. (1967). A model for trips generated by households.
235
+ *Journal of Transport Economics and Policy* 1(2), 137-153.
236
+
237
+ ## Academic citation
238
+
239
+ ```bibtex
240
+ @software{eminoglu_tripdist,
241
+ author = {Eminoğlu, Yusuf},
242
+ title = {tripdist},
243
+ year = {2026},
244
+ version = {0.1.0},
245
+ url = {https://gitlab.com/geophilo1/tripdist_sdk}
246
+ }
247
+ ```
248
+
249
+ ## License
250
+
251
+ MIT, see [LICENSE](LICENSE).
@@ -0,0 +1,216 @@
1
+ # tripdist
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/tripdist.svg)](https://pypi.org/project/tripdist/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/tripdist.svg)](https://pypi.org/project/tripdist/)
5
+ [![pipeline](https://gitlab.com/geophilo1/tripdist_sdk/badges/main/pipeline.svg)](https://gitlab.com/geophilo1/tripdist_sdk/-/pipelines)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://gitlab.com/geophilo1/tripdist_sdk/-/blob/main/LICENSE)
7
+
8
+ **Trip generation and distribution (stages 1 and 2 of the four-step model) in plain NumPy.**
9
+
10
+ `tripdist` covers the first two stages of the classic four-step travel demand model as
11
+ set out in Ortúzar & Willumsen, *Modelling Transport*:
12
+
13
+ - **Trip generation**: zonal or household linear regression with standard errors, t
14
+ statistics and R²; cross-classification (category analysis) trip rates; balancing of
15
+ productions and attractions.
16
+ - **Growth-factor methods**: uniform, singly constrained, average factor, Detroit, Fratar
17
+ and Furness.
18
+ - **Gravity models**: unconstrained, production-constrained, attraction-constrained and
19
+ doubly constrained (Furness / IPF balancing), with exponential, power, combined (Tanner),
20
+ gamma and tabulated deterrence functions, K-factors and intrazonal handling.
21
+ - **Calibration**: Hyman's (1969) method for a deterrence parameter against an observed
22
+ mean trip cost, iterative fitting of stepwise friction factors to an observed trip
23
+ length frequency distribution, ratio K-factors at zone or district level.
24
+ - **Diagnostics**: Furness convergence history, trip length frequency distributions and
25
+ the coincidence ratio, RMSE / %RMSE / R² / GEH matrix comparison, district aggregation.
26
+
27
+ It needs only NumPy. A command-line tool reads CSV matrices and zone tables and writes JSON
28
+ and CSV.
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ pip install tripdist
34
+ ```
35
+
36
+ Requires Python 3.9 or newer.
37
+
38
+ ## Quickstart
39
+
40
+ ```python
41
+ import numpy as np
42
+ import tripdist as td
43
+
44
+ cost = np.array([[3, 11, 18, 22],
45
+ [12, 3, 13, 19],
46
+ [15.5, 13, 5, 7],
47
+ [24, 18, 8, 5]], dtype=float) # minutes
48
+ productions = np.array([98, 106, 122, 174], dtype=float)
49
+ attractions = np.array([102, 118, 141, 139], dtype=float)
50
+
51
+ # doubly constrained gravity model with an exponential deterrence function
52
+ res = td.gravity(cost, productions, attractions, td.exponential(0.1))
53
+ res.matrix, res.iterations, res.converged, res.mean_cost
54
+ res.history # largest relative margin error per Furness pass
55
+
56
+ # calibrate beta so the model reproduces an observed mean trip cost (Hyman 1969)
57
+ cal = td.hyman(cost, productions, attractions, target_mean_cost=10.0)
58
+ cal.value, cal.modelled_mean_cost, cal.trace
59
+
60
+ # compare trip length distributions with an observed matrix
61
+ edges = td.cost_bins(cost, 5.0)
62
+ cmp = td.compare_tlfd(cal.model.matrix, observed_matrix, cost, edges)
63
+ cmp.coincidence_ratio
64
+ ```
65
+
66
+ Trip generation and growth factors:
67
+
68
+ ```python
69
+ m = td.fit_regression(trips, np.column_stack([households, cars]), ["households", "cars"])
70
+ m.coefficients, m.t_stats, m.r_squared
71
+ rates = td.cross_classification_rates(hh_trips, list(zip(car_ownership, hh_size)))
72
+ p = td.apply_category_rates(households_by_category, rates, category_keys)
73
+ p, a = td.balance_pa(p, a, method="productions")
74
+
75
+ td.furness_growth(base, origin_totals=o_future, dest_totals=d_future)
76
+ td.fratar(base, origin_factors=tau, dest_factors=tau)
77
+ td.growth_factor(base, "detroit", origin_totals=o_future, dest_totals=d_future)
78
+ ```
79
+
80
+ Other deterrence functions and options:
81
+
82
+ ```python
83
+ td.power(2.0) # c^-n
84
+ td.combined(-0.5, 0.05) # Tanner: c^n exp(-beta c)
85
+ td.gamma(a=1.0, b=0.5, g=0.05) # a c^-b exp(-g c); parameters are yours
86
+ td.tabular([0, 5, 10, 20], [1.0, 0.6, 0.2])
87
+ td.gravity(cost, p, a, {"form": "exponential", "beta": 0.08},
88
+ constraint="production", k_factors=k, intrazonal="exclude")
89
+ td.intrazonal_costs(cost, factor=0.5, neighbours=1) # factor is a modelling choice
90
+ ```
91
+
92
+ ## Command line
93
+
94
+ ```bash
95
+ tripdist balance --zones zones.csv --method productions --csv balanced.csv
96
+ tripdist growth base.csv --method furness --targets targets.csv --matrix-out future.csv
97
+ tripdist gravity cost.csv --zones zones.csv --form exponential --beta 0.1 \
98
+ --observed observed.csv --bin-width 5 --matrix-out modelled.csv -o report.json
99
+ tripdist calibrate cost.csv --zones zones.csv --observed observed.csv
100
+ tripdist calibrate cost.csv --zones zones.csv --target-mean 14 --form power --n 1 \
101
+ --parameter n --start 1 --zero-cost-value 1
102
+ tripdist tlfd modelled.csv cost.csv --edges 0,5,10,20,40 --observed observed.csv --csv tlfd.csv
103
+ ```
104
+
105
+ Matrices are CSV grids, either plain or with a header row and a first column of zone
106
+ labels (give the top-left cell a name such as `zone`); empty cells are 0 and `inf` marks
107
+ impossible movements. Zone tables are CSV files with a header (`zone,productions,
108
+ attractions` by default; growth targets use `origins,destinations`). Deterrence functions
109
+ come from `--form` with its parameters or from a JSON file such as
110
+ `{"form": "gamma", "a": 1, "b": 0.5, "g": 0.05}`. Every command prints JSON or writes it
111
+ with `-o`.
112
+
113
+ ## API overview
114
+
115
+ | Area | Functions / classes |
116
+ | --- | --- |
117
+ | Generation | `fit_regression -> RegressionModel`, `cross_classification_rates -> CategoryRates`, `apply_category_rates`, `balance_pa` |
118
+ | Balancing | `furness -> BalanceResult`, `margin_error` |
119
+ | Growth factors | `uniform_growth`, `singly_constrained_growth`, `average_factor`, `detroit`, `fratar`, `furness_growth`, `growth_factor -> GrowthResult` |
120
+ | Deterrence | `exponential`, `power`, `combined` / `tanner`, `gamma`, `tabular`, `deterrence_from_dict -> Deterrence` |
121
+ | Gravity | `gravity -> GravityResult`, `mean_cost`, `intrazonal_costs`, `intrazonal_share` |
122
+ | Calibration | `hyman -> CalibrationResult`, `observed_mean_cost`, `calibrate_friction_factors -> FrictionCalibration`, `k_factors` |
123
+ | TLFD | `tlfd -> TLFD`, `compare_tlfd -> TLFDComparison`, `coincidence_ratio`, `cost_bins` |
124
+ | Diagnostics | `summarize`, `compare_matrices -> MatrixFit`, `geh`, `aggregate` |
125
+ | I/O | `read_matrix`, `write_matrix`, `read_zone_table`, `read_long`, `read_json`, `write_json` |
126
+
127
+ All result objects have `to_dict()` returning JSON-safe values (NaN and infinity become
128
+ `null`).
129
+
130
+ ## Methods and sources
131
+
132
+ | Method | Definition | Source |
133
+ | --- | --- | --- |
134
+ | Regression trip generation | OLS `y = b0 + sum b_k x_k`, with SE, t, R², adjusted R² | Ortúzar & Willumsen (2011), ch. 4 |
135
+ | Cross-classification | mean trips per household by category; `P_i = sum_c H_ic t_c` | Wootton & Pick (1967) |
136
+ | P/A balancing | scale attractions to productions (or the reverse, or the mean); optional fixed zones | Ortúzar & Willumsen (2011), ch. 4 |
137
+ | Uniform, singly constrained | `T = tau t`; `T_ij = tau_i t_ij` | Ortúzar & Willumsen (2011), §5.2 |
138
+ | Average factor | `T_ij = t_ij (tau_i + tau_j) / 2`, iterated | Ortúzar & Willumsen (2011), §5.2 |
139
+ | Detroit | `T_ij = t_ij tau_i tau_j / tau`, iterated | Detroit Metropolitan Area Traffic Study (1956), via Ortúzar & Willumsen |
140
+ | Fratar | `T_ij = t_ij tau_i tau_j (L_i + L_j) / 2`, `L_i = sum_j t_ij / sum_j t_ij tau_j`, iterated | Fratar (1954) |
141
+ | Furness / IPF | `T_ij = A_i B_j t_ij`, alternate row and column scaling | Furness (1965); Deming & Stephan (1940); Bacharach (1970) |
142
+ | Gravity model | `T_ij = A_i O_i B_j D_j f(c_ij)` and its singly / unconstrained forms | Wilson (1970); Ortúzar & Willumsen (2011), §5.3 |
143
+ | Deterrence | `exp(-beta c)`, `c^-n`, `c^n exp(-beta c)` | Ortúzar & Willumsen (2011), ch. 5; Tanner (1961) |
144
+ | Gamma deterrence | `a c^-b exp(-g c)`, i.e. NCHRP 716's `a t^b exp(c t)` with `b = -b_716`, `g = -c_716` (form only; no parameter tables included) | NCHRP Report 716 (2012) |
145
+ | Hyman calibration | `beta_0 = 1/c*`, `beta_1 = beta_0 c_0 / c*`, then secant updates on the mean cost | Hyman (1969); Ortúzar & Willumsen (2011), §5.5 |
146
+ | Friction-factor fitting | `F_k <- F_k p_k(obs) / p_k(mod)` per cost band | FHWA (1965) |
147
+ | K-factors | ratio `observed / modelled`, by zone or district pair | FHWA (1965) (simple ratio form) |
148
+ | Coincidence ratio | `sum min(p_k, q_k) / sum max(p_k, q_k)` | FHWA Travel Model Validation Manual (2010) |
149
+ | GEH | `sqrt(2 (M - O)^2 / (M + O))` | UK DfT TAG unit M3.1 |
150
+
151
+ Notes:
152
+
153
+ - The Furness iteration stops when the largest relative margin error falls below `tol`
154
+ (default `1e-6`). Unequal target totals raise an error unless `total_mismatch` says how
155
+ to reconcile them. Seed rows or columns that are all zero but have positive targets
156
+ raise an error; other infeasible zero patterns are reported as `converged=False`.
157
+ - Infinite costs give `f = 0`. Power-type deterrence at `c = 0` raises an error unless
158
+ intrazonal costs are set or `zero_cost_value` is given.
159
+ - In Hyman's method the start value `1 / c*` applies to the exponential `beta`; for other
160
+ forms give `start`. If a secant step would make a positive parameter non-positive, the
161
+ step is halved towards zero instead.
162
+ - `intrazonal_costs` replaces the diagonal by a fraction of the cost to the nearest
163
+ zone(s); the fraction is yours to choose.
164
+
165
+ ## Limitations
166
+
167
+ - No default parameter values: published parameter sets (for example the NCHRP 716 gamma
168
+ tables) are not bundled; calibrate or supply your own.
169
+ - No acceptance thresholds for coincidence ratios, %RMSE or GEH are built in; they differ
170
+ between guidance documents.
171
+ - Dense matrices only; large zone systems (tens of thousands of zones) need care with
172
+ memory.
173
+ - No mode choice or assignment: this package covers stages 1 and 2 only.
174
+
175
+ ## References
176
+
177
+ - Bacharach, M. (1970). *Biproportional Matrices and Input-Output Change*. Cambridge
178
+ University Press.
179
+ - Cambridge Systematics (2010). *Travel Model Validation and Reasonableness Checking
180
+ Manual*, 2nd ed. Federal Highway Administration, Washington, DC.
181
+ - Cambridge Systematics et al. (2012). *NCHRP Report 716: Travel Demand Forecasting:
182
+ Parameters and Techniques*. Transportation Research Board, Washington, DC.
183
+ - Deming, W. E. & Stephan, F. F. (1940). On a least squares adjustment of a sampled
184
+ frequency table when the expected marginal totals are known. *Annals of Mathematical
185
+ Statistics* 11(4), 427-444.
186
+ - Federal Highway Administration (1965). *Calibrating and Testing a Gravity Model for Any
187
+ Size Urban Area*. US Department of Commerce, Bureau of Public Roads.
188
+ - Fratar, T. J. (1954). Vehicular trip distribution by successive approximations.
189
+ *Traffic Quarterly* 8(1), 53-65.
190
+ - Furness, K. P. (1965). Time function iteration. *Traffic Engineering and Control* 7(7),
191
+ 458-460.
192
+ - Hyman, G. M. (1969). The calibration of trip distribution models. *Environment and
193
+ Planning* 1(1), 105-112.
194
+ - Ortúzar, J. de D. & Willumsen, L. G. (2011). *Modelling Transport*, 4th ed. Wiley,
195
+ Chichester.
196
+ - Tanner, J. C. (1961). *Factors affecting the amount of travel*. Road Research Technical
197
+ Paper 51, HMSO, London.
198
+ - Wilson, A. G. (1970). *Entropy in Urban and Regional Modelling*. Pion, London.
199
+ - Wootton, H. J. & Pick, G. W. (1967). A model for trips generated by households.
200
+ *Journal of Transport Economics and Policy* 1(2), 137-153.
201
+
202
+ ## Academic citation
203
+
204
+ ```bibtex
205
+ @software{eminoglu_tripdist,
206
+ author = {Eminoğlu, Yusuf},
207
+ title = {tripdist},
208
+ year = {2026},
209
+ version = {0.1.0},
210
+ url = {https://gitlab.com/geophilo1/tripdist_sdk}
211
+ }
212
+ ```
213
+
214
+ ## License
215
+
216
+ MIT, see [LICENSE](LICENSE).
@@ -0,0 +1,56 @@
1
+ """tripdist quickstart on a small synthetic city (no input files needed).
2
+
3
+ Run: python examples/quickstart.py
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import numpy as np
9
+
10
+ import tripdist as td
11
+
12
+
13
+ def main() -> None:
14
+ # 1. Trip generation: a zonal regression of productions on households and cars
15
+ households = np.array([1200, 950, 600, 1500, 800, 400], dtype=float)
16
+ cars = np.array([700, 500, 450, 1000, 300, 350], dtype=float)
17
+ observed_productions = np.array([2400, 1800, 1350, 3050, 1350, 950], dtype=float)
18
+ model = td.fit_regression(
19
+ observed_productions, np.column_stack([households, cars]), ["households", "cars"]
20
+ )
21
+ print(f"Regression R2 = {model.r_squared:.3f}, coefficients {model.coefficients.round(3)}")
22
+ productions = model.predict(np.column_stack([households, cars]))
23
+ jobs = np.array([3000, 900, 700, 2500, 1500, 400], dtype=float)
24
+ productions, attractions = td.balance_pa(productions, jobs, method="productions")
25
+
26
+ # 2. A cost skim (minutes) with intrazonal costs from the nearest neighbour
27
+ xy = np.array([[0, 0], [4, 1], [8, 0], [2, 5], [6, 6], [10, 4]], dtype=float)
28
+ cost = 3.0 * np.hypot(*(xy[:, None, :] - xy[None, :, :]).transpose(2, 0, 1))
29
+ cost = td.intrazonal_costs(cost, factor=0.5)
30
+
31
+ # 3. Doubly constrained gravity model, calibrated to an observed mean trip time
32
+ calib = td.hyman(cost, productions, attractions, target_mean_cost=14.0)
33
+ print(
34
+ f"Hyman: beta = {calib.value:.4f} after {calib.iterations} runs, "
35
+ f"mean cost {calib.modelled_mean_cost:.2f} min"
36
+ )
37
+ trips = calib.model.matrix
38
+ print("Furness iterations:", calib.model.iterations)
39
+ print("Intrazonal share:", round(td.intrazonal_share(trips), 3))
40
+
41
+ # 4. Trip length frequency distribution
42
+ dist = td.tlfd(trips, cost, td.cost_bins(cost, 5.0))
43
+ for band in dist.records():
44
+ print(f" {band['lower']:4.0f}-{band['upper']:4.0f} min {band['share']:6.1%}")
45
+
46
+ # 5. Grow the matrix to a horizon year with the Furness method
47
+ horizon = td.furness_growth(
48
+ trips,
49
+ origin_totals=productions * 1.2,
50
+ dest_totals=attractions * 1.2,
51
+ )
52
+ print(f"Horizon total {horizon.total:.0f} trips, converged={horizon.converged}")
53
+
54
+
55
+ if __name__ == "__main__":
56
+ main()