retailgravity 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 (42) hide show
  1. retailgravity-0.1.0/CHANGELOG.md +28 -0
  2. retailgravity-0.1.0/CITATION.cff +22 -0
  3. retailgravity-0.1.0/LICENSE +21 -0
  4. retailgravity-0.1.0/MANIFEST.in +9 -0
  5. retailgravity-0.1.0/PKG-INFO +264 -0
  6. retailgravity-0.1.0/README.md +227 -0
  7. retailgravity-0.1.0/examples/origins.csv +17 -0
  8. retailgravity-0.1.0/examples/quickstart.py +80 -0
  9. retailgravity-0.1.0/examples/stores.csv +5 -0
  10. retailgravity-0.1.0/pyproject.toml +114 -0
  11. retailgravity-0.1.0/setup.cfg +4 -0
  12. retailgravity-0.1.0/src/retailgravity/__init__.py +120 -0
  13. retailgravity-0.1.0/src/retailgravity/__main__.py +7 -0
  14. retailgravity-0.1.0/src/retailgravity/_arrays.py +80 -0
  15. retailgravity-0.1.0/src/retailgravity/_version.py +1 -0
  16. retailgravity-0.1.0/src/retailgravity/calibrate.py +279 -0
  17. retailgravity-0.1.0/src/retailgravity/cli.py +349 -0
  18. retailgravity-0.1.0/src/retailgravity/distance.py +132 -0
  19. retailgravity-0.1.0/src/retailgravity/huff.py +243 -0
  20. retailgravity-0.1.0/src/retailgravity/io.py +207 -0
  21. retailgravity-0.1.0/src/retailgravity/mci.py +237 -0
  22. retailgravity-0.1.0/src/retailgravity/py.typed +0 -0
  23. retailgravity-0.1.0/src/retailgravity/reilly.py +188 -0
  24. retailgravity-0.1.0/src/retailgravity/scenario.py +304 -0
  25. retailgravity-0.1.0/src/retailgravity/tradearea.py +190 -0
  26. retailgravity-0.1.0/src/retailgravity/wilson.py +283 -0
  27. retailgravity-0.1.0/src/retailgravity.egg-info/PKG-INFO +264 -0
  28. retailgravity-0.1.0/src/retailgravity.egg-info/SOURCES.txt +40 -0
  29. retailgravity-0.1.0/src/retailgravity.egg-info/dependency_links.txt +1 -0
  30. retailgravity-0.1.0/src/retailgravity.egg-info/entry_points.txt +2 -0
  31. retailgravity-0.1.0/src/retailgravity.egg-info/requires.txt +7 -0
  32. retailgravity-0.1.0/src/retailgravity.egg-info/top_level.txt +1 -0
  33. retailgravity-0.1.0/tests/test_calibrate.py +149 -0
  34. retailgravity-0.1.0/tests/test_cli.py +176 -0
  35. retailgravity-0.1.0/tests/test_distance.py +75 -0
  36. retailgravity-0.1.0/tests/test_huff.py +112 -0
  37. retailgravity-0.1.0/tests/test_io.py +112 -0
  38. retailgravity-0.1.0/tests/test_mci.py +139 -0
  39. retailgravity-0.1.0/tests/test_reilly.py +77 -0
  40. retailgravity-0.1.0/tests/test_scenario.py +117 -0
  41. retailgravity-0.1.0/tests/test_tradearea.py +111 -0
  42. retailgravity-0.1.0/tests/test_wilson.py +174 -0
@@ -0,0 +1,28 @@
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
+ - Huff model (`huff`, `huff_probabilities`): power or exponential distance decay,
10
+ attractiveness exponent, expected patronage, store sales, market share, fair share,
11
+ choice-set exclusion through infinite distances.
12
+ - MCI model (`mci_probabilities`) and Nakanishi & Cooper log-centering OLS calibration
13
+ (`fit_mci`) with standard errors on fixed-effects degrees of freedom; `fit_huff` for
14
+ Huff's `alpha` and `beta` (optionally with `alpha` fixed).
15
+ - Reilly's law (`reilly_ratio`, `reilly_shares`) and Converse breaking points
16
+ (`breaking_point`, `breaking_point_location` on the great circle or straight line,
17
+ pairwise `breaking_points`).
18
+ - Wilson family (`spatial_interaction`): unconstrained, production-, attraction- and
19
+ doubly constrained models; Furness / IPF balancing (`furness`) with convergence report.
20
+ - Distance-decay calibration (`calibrate_beta`): Poisson maximum likelihood and
21
+ mean-trip-length matching for all four model types, with log-likelihood, R² and SRMSE.
22
+ - Trade areas: dominant-store assignment, probability contours and bands,
23
+ cumulative-patronage primary/secondary trade areas, gridded probability surfaces.
24
+ - What-if scenarios (`Market`, `Store`, `compare`): add, remove and resize stores;
25
+ cannibalisation, competitor draw and net chain gain.
26
+ - Haversine and Euclidean distance matrices; CSV and GeoJSON point input.
27
+ - Command-line interface `retailgravity` with `huff`, `tradearea`, `breakpoint`,
28
+ `calibrate` and `scenario` commands.
@@ -0,0 +1,22 @@
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: "retailgravity"
9
+ version: "0.1.0"
10
+ date-released: "2026-11-09"
11
+ license: "MIT"
12
+ repository-code: "https://github.com/YusufEminoglu/retailgravity_sdk"
13
+ url: "https://pypi.org/project/retailgravity/"
14
+ abstract: "Spatial interaction and retail catchment models for site analysis: the Huff model, Multiplicative Competitive Interaction (MCI) with Nakanishi-Cooper log-centering calibration, Reilly's law and Converse breaking points, Wilson's family of unconstrained, production-, attraction- and doubly constrained gravity models with Furness balancing, Poisson maximum-likelihood and mean-trip-length calibration of distance decay, trade-area delineation and what-if scenarios with cannibalisation."
15
+ keywords:
16
+ - huff model
17
+ - retail gravitation
18
+ - spatial interaction
19
+ - gravity model
20
+ - trade area
21
+ - site selection
22
+ - urban planning
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yusuf Eminoğlu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,9 @@
1
+ include LICENSE
2
+ include README.md
3
+ include CHANGELOG.md
4
+ include CITATION.cff
5
+ include pyproject.toml
6
+ include src/retailgravity/py.typed
7
+ recursive-include src/retailgravity *.py
8
+ recursive-include tests *.py
9
+ recursive-include examples *.py *.csv
@@ -0,0 +1,264 @@
1
+ Metadata-Version: 2.4
2
+ Name: retailgravity
3
+ Version: 0.1.0
4
+ Summary: Spatial interaction and retail catchment models for site analysis: Huff, MCI with log-centering calibration, Reilly/Converse breaking points, Wilson's constrained gravity models with Furness balancing, distance-decay calibration, trade areas and what-if scenarios.
5
+ Author-email: Yusuf Eminoğlu <geospacephilo@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/YusufEminoglu/retailgravity_sdk
8
+ Project-URL: Repository, https://github.com/YusufEminoglu/retailgravity_sdk
9
+ Project-URL: Issues, https://github.com/YusufEminoglu/retailgravity_sdk/issues
10
+ Project-URL: Changelog, https://github.com/YusufEminoglu/retailgravity_sdk/blob/main/CHANGELOG.md
11
+ Keywords: huff-model,retail-gravitation,spatial-interaction,gravity-model,mci,reilly,breaking-point,wilson,furness,trade-area,catchment,site-selection,market-share,cannibalization,gis,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 :: GIS
22
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
23
+ Classifier: Topic :: Office/Business
24
+ Classifier: Intended Audience :: Science/Research
25
+ Classifier: Intended Audience :: Developers
26
+ Classifier: Typing :: Typed
27
+ Requires-Python: >=3.9
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+ Requires-Dist: numpy>=1.21
31
+ Provides-Extra: dev
32
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
33
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
34
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
35
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
36
+ Dynamic: license-file
37
+
38
+ # retailgravity
39
+
40
+ [![PyPI version](https://img.shields.io/pypi/v/retailgravity.svg)](https://pypi.org/project/retailgravity/)
41
+ [![Python versions](https://img.shields.io/pypi/pyversions/retailgravity.svg)](https://pypi.org/project/retailgravity/)
42
+ [![CI](https://github.com/YusufEminoglu/retailgravity_sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/YusufEminoglu/retailgravity_sdk/actions/workflows/ci.yml)
43
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
44
+
45
+ **Spatial interaction and retail catchment models for site analysis, in Python.**
46
+
47
+ `retailgravity` collects the classic retail-geography toolbox in one small, typed
48
+ package that depends on NumPy only:
49
+
50
+ - **Huff model**: patronage probabilities, expected patronage, store sales, market and fair share.
51
+ - **MCI** (Multiplicative Competitive Interaction) with the Nakanishi & Cooper **log-centering OLS** calibration. Huff's `alpha` and `beta` are estimated the same way.
52
+ - **Reilly's law** of retail gravitation and **Converse breaking points**, located on the great circle between towns.
53
+ - **Wilson's family** of spatial interaction models: unconstrained, production-, attraction- and doubly constrained (**Furness / IPF** balancing), with power or exponential deterrence.
54
+ - **Calibration of distance decay** to observed flows by Poisson **maximum likelihood** or **mean trip length** matching, reporting log-likelihood, R² and SRMSE.
55
+ - **Trade areas**: dominant-store assignment, probability contours as point sets, primary and secondary trade areas from cumulative patronage, and gridded probability surfaces.
56
+ - **What-if scenarios**: add, remove or resize stores and measure **cannibalisation**, competitor draw and net chain gain.
57
+ - Origins and stores come from lists, **CSV** or **GeoJSON** points. Distances are supplied as a matrix or computed (haversine for lon/lat, or Euclidean).
58
+ - A `retailgravity` command-line tool.
59
+
60
+ ```bash
61
+ pip install retailgravity
62
+ ```
63
+
64
+ Requires Python 3.9 or newer and NumPy.
65
+
66
+ ---
67
+
68
+ ## Quick start (Python)
69
+
70
+ ```python
71
+ from retailgravity import read_locations, attribute_array, distance_matrix, huff
72
+
73
+ origins = read_locations("examples/origins.csv") # id, lon, lat, population
74
+ stores = read_locations("examples/stores.csv") # id, lon, lat, floor_area, chain
75
+ d = distance_matrix(origins, stores) # haversine, km
76
+
77
+ res = huff(d, attribute_array(stores, "floor_area"), attribute_array(origins, "population"),
78
+ alpha=1.0, beta=2.0, store_ids=[s.id for s in stores])
79
+ res.probabilities # (origins x stores); every row sums to 1
80
+ res.sales # expected customers per store
81
+ res.market_share # sums to 1
82
+ res.store_table() # sales, market share, fair share, share index
83
+ ```
84
+
85
+ Calibrating, delineating and testing scenarios:
86
+
87
+ ```python
88
+ from retailgravity import calibrate_beta, fit_huff, cumulative_trade_area, Market, Store, compare
89
+
90
+ # beta from observed origin-store flows (production-constrained = Huff with spending O_i)
91
+ cal = calibrate_beta(observed, d, constraint="production", deterrence_kind="power",
92
+ destination_mass=floor_area)
93
+ cal.beta, cal.log_likelihood, cal.r_squared, cal.srmse
94
+
95
+ # alpha and beta from observed shares via MCI log-centering regression
96
+ fit = fit_huff(observed_shares, d, floor_area)
97
+ fit.alpha, fit.beta, fit.mci.summary()
98
+
99
+ # primary (60 %) / secondary (85 %) trade area of store 0, nearest origins first
100
+ labels = cumulative_trade_area(res.patronage[:, 0], order_by=d[:, 0])
101
+
102
+ # a new store for chain "Alpha"
103
+ market = Market.from_coordinates(origins, population, [Store("S1", 12000, 27.07, 38.41, "Alpha"), ...])
104
+ print(compare(market, market.add_store(Store("S5", 6000, 27.15, 38.47, "Alpha"))).summary())
105
+ ```
106
+
107
+ `python examples/quickstart.py` runs all of this on the bundled synthetic data.
108
+
109
+ ## Quick start (command line)
110
+
111
+ ```bash
112
+ # Huff: store summary to stdout, long origin-store table to CSV
113
+ retailgravity huff --origins examples/origins.csv --stores examples/stores.csv \
114
+ --demand-col population --attractiveness-col floor_area --beta 2 -o huff.csv
115
+
116
+ # Trade area of S1 as GeoJSON points (dominant store, probability band, primary/secondary)
117
+ retailgravity tradearea --origins examples/origins.csv --stores examples/stores.csv \
118
+ --demand-col population --attractiveness-col floor_area --store S1 \
119
+ --levels 0.75,0.5,0.25 --cumulative 0.6,0.85 -o s1_trade_area.geojson
120
+
121
+ # What-if: Alpha opens S5; S2 is enlarged by 50 %
122
+ retailgravity scenario --origins examples/origins.csv --stores examples/stores.csv \
123
+ --demand-col population --attractiveness-col floor_area --chain-col chain \
124
+ --add S5,27.15,38.47,6000,Alpha --resize "S2*1.5"
125
+
126
+ # Breaking points between every pair of towns
127
+ retailgravity breakpoint --places towns.csv --size-col population
128
+
129
+ # Calibrate beta from a long flow table (origin, destination, flow, cost)
130
+ retailgravity calibrate --flows flows.csv --constraint doubly --deterrence exponential
131
+ ```
132
+
133
+ Output of the scenario command on the example data:
134
+
135
+ ```
136
+ store chain base scenario change %
137
+ S1 Alpha 79136.22 72927.53 -6208.69 -7.8
138
+ S2 Beta 66232.10 53157.53 -13074.57 -19.7
139
+ S3 Alpha 73996.17 62843.20 -11152.98 -15.1
140
+ S4 Gamma 57635.51 52495.37 -5140.14 -8.9
141
+ S5 Alpha 0.00 35576.38 35576.38
142
+ new store sales: 35576.38
143
+ cannibalisation (own chain): 17361.67
144
+ drawn from competitors: 18214.71
145
+ cannibalisation rate: 48.8 %
146
+ net change for chain Alpha: 18214.71
147
+ ```
148
+
149
+ Precomputed network distances or travel times can replace the straight-line
150
+ distances: pass a matrix in Python, or `--distances` with a long CSV
151
+ (`origin_id,store_id,distance`) on the command line.
152
+
153
+ ## API overview
154
+
155
+ | Area | Functions / classes |
156
+ |---|---|
157
+ | Distances | `distance_matrix`, `haversine_matrix`, `euclidean_matrix`, `interpolate` |
158
+ | Input/output | `read_locations` (CSV/GeoJSON), `locations_from_records`, `Location`, `attribute_array`, `coordinates`, `write_csv` |
159
+ | Huff | `huff` → `HuffResult`, `huff_probabilities`, `utilities`, `deterrence` |
160
+ | MCI | `mci_probabilities`, `fit_mci` → `MCIFit`, `fit_huff` → `HuffFit` |
161
+ | Reilly / Converse | `reilly_ratio`, `reilly_shares`, `breaking_point`, `breaking_point_location`, `breaking_points` |
162
+ | Wilson family | `spatial_interaction` → `SIResult`, `furness`, `mean_cost`, `ConvergenceWarning` |
163
+ | Calibration | `calibrate_beta` → `CalibrationResult`, `poisson_log_likelihood`, `goodness_of_fit` |
164
+ | Trade areas | `dominant_store`, `contour_sets`, `probability_bands`, `cumulative_trade_area`, `probability_surface` → `ProbabilitySurface` |
165
+ | Scenarios | `Market`, `Store`, `compare` → `ScenarioComparison` |
166
+
167
+ ## Methods and sources
168
+
169
+ **Huff model** (Huff 1963, 1964). `P_ij = S_j^α f(d_ij) / Σ_k S_k^α f(d_ik)` with
170
+ `f(d) = d^-β` (Huff's λ is called `beta` here) or `exp(-β d)`. Expected patronage
171
+ `E_ij = C_i P_ij`. An infinite distance takes a store out of an origin's choice set. A zero
172
+ distance with the power form needs `min_distance`.
173
+
174
+ **MCI and log-centering** (Nakanishi & Cooper 1974; Cooper & Nakanishi 1988).
175
+ `P_ij = Π_k X_kij^β_k / Σ_l Π_k X_kil^β_k`. Dividing by the geometric mean of each choice
176
+ set gives `log(p_ij/p̃_i) = Σ_k β_k log(X_kij/X̃_ki)`, which is estimated by OLS without an
177
+ intercept. This equals a regression on logs with one dummy per choice set, so the residual
178
+ degrees of freedom are `N − I − K`. The test suite checks the equivalence numerically. Zero
179
+ shares cannot be logged and are dropped.
180
+
181
+ **Reilly and Converse** (Reilly 1931; Converse 1949). `B_a/B_b = (P_a/P_b)(D_b/D_a)²`.
182
+ The breaking point measured from the smaller place B is `D_B = D_AB / (1 + √(P_A/P_B))`.
183
+ Both exponents can be changed.
184
+
185
+ **Wilson's family** (Wilson 1971; Fotheringham & O'Kelly 1989):
186
+ unconstrained `T_ij = k V_i^μ W_j^α f(c_ij)`; production-constrained
187
+ `T_ij = A_i O_i W_j^α f(c_ij)` (the retail model, identical to Huff with power deterrence);
188
+ attraction-constrained `T_ij = B_j D_j V_i^μ f(c_ij)`; doubly constrained
189
+ `T_ij = A_i O_i B_j D_j f(c_ij)`, balanced by Furness (1965) iterations until both margins
190
+ are met to a relative tolerance of 1e-9.
191
+
192
+ **Calibration** (Hyman 1969; Batty & Mackie 1972). When the balancing factors (or `k`)
193
+ are free, the Poisson ML estimate of β reproduces the observed flow-weighted mean of
194
+ `c` (exponential) or `ln c` (power). `calibrate_beta(method="mle")` solves that
195
+ monotone equation by bisection. `method="mean_trip_length"` matches the mean cost
196
+ (Hyman's criterion) and gives the same answer for exponential deterrence. Fit is reported
197
+ as the Poisson log-likelihood, R² and SRMSE (Knudsen & Fotheringham 1986).
198
+
199
+ **Trade areas.** Huff mapped trade areas as equal-probability contours.
200
+ `contour_sets` and `ProbabilitySurface.contour_points` return them as point sets.
201
+ `cumulative_trade_area` ranks origins by distance or by contribution and cuts at
202
+ cumulative shares of the store's patronage. The default 60 % / 85 % primary/secondary
203
+ cut-offs are a common rule of thumb, not a published standard. Set them for your study.
204
+
205
+ **Scenarios.** Total demand is fixed, so a new store's sales always equal the losses of
206
+ existing stores. These losses split into cannibalisation (own chain) and competitor draw.
207
+
208
+ References:
209
+
210
+ - Huff, D. L. (1963). A probabilistic analysis of shopping center trade areas. *Land Economics*, 39(1), 81–90.
211
+ - Huff, D. L. (1964). Defining and estimating a trading area. *Journal of Marketing*, 28(3), 34–38.
212
+ - Nakanishi, M., & Cooper, L. G. (1974). Parameter estimation for a multiplicative competitive interaction model: least squares approach. *Journal of Marketing Research*, 11(3), 303–311.
213
+ - Cooper, L. G., & Nakanishi, M. (1988). *Market-Share Analysis*. Kluwer.
214
+ - Reilly, W. J. (1931). *The Law of Retail Gravitation*. New York: Knickerbocker Press.
215
+ - Converse, P. D. (1949). New laws of retail gravitation. *Journal of Marketing*, 14, 379–384.
216
+ - Wilson, A. G. (1971). A family of spatial interaction models, and associated developments. *Environment and Planning A*, 3(1), 1–32.
217
+ - Furness, K. P. (1965). Time function iteration. *Traffic Engineering and Control*, 7(7), 458–460.
218
+ - Hyman, G. M. (1969). The calibration of trip distribution models. *Environment and Planning*, 1(1), 105–112.
219
+ - Batty, M., & Mackie, S. (1972). The calibration of gravity, entropy, and related models of spatial interaction. *Environment and Planning*, 4(2), 205–233.
220
+ - Fotheringham, A. S., & O'Kelly, M. E. (1989). *Spatial Interaction Models: Formulations and Applications*. Kluwer.
221
+ - Knudsen, D. C., & Fotheringham, A. S. (1986). Matrix comparison, goodness-of-fit, and spatial interaction modeling. *International Regional Science Review*, 10(2), 127–147.
222
+ - Sinnott, R. W. (1984). Virtues of the haversine. *Sky and Telescope*, 68(2), 159.
223
+
224
+ ## Limitations
225
+
226
+ - Distances are straight-line (spherical haversine, mean Earth radius 6371.0088 km, or planar Euclidean). For network distances or travel times, supply your own matrix (for example from a routing engine).
227
+ - Demand is fixed. The models redistribute spending and do not create new spending, so they do not model market expansion or outshopping beyond the study area. Choose a study area large enough to avoid edge effects.
228
+ - Huff and MCI assume independence from irrelevant alternatives (IIA). Closely competing nearby stores can be over-predicted.
229
+ - MCI calibration needs strictly positive shares and attributes. Zero shares are dropped, which biases estimates when zeros are common.
230
+ - `calibrate_beta` estimates one global β. It does not estimate local or origin-specific decay, the attractiveness exponent of constrained models, or confidence intervals.
231
+ - The 60 % / 85 % cumulative trade-area defaults are a convention and must be overridden as needed.
232
+
233
+ ## Development
234
+
235
+ ```bash
236
+ pip install -e ".[dev]"
237
+ pytest --cov=retailgravity
238
+ ruff check . && ruff format --check src tests && mypy src
239
+ ```
240
+
241
+ The tests check worked examples against hand-computed values. Examples include Huff
242
+ probabilities of 6/11, 3/11 and 2/11, a Converse breaking point of 10 miles, the Furness
243
+ closed form `O_i D_j / T`, and exact recovery of known MCI and β parameters. They also
244
+ check invariants: rows sum to 1, constrained margins are reproduced, and cannibalisation
245
+ plus competitor draw equals the new store's sales.
246
+
247
+ ## License
248
+
249
+ MIT © 2026 Yusuf Eminoğlu. See [LICENSE](LICENSE). To cite retailgravity, see
250
+ [CITATION.cff](CITATION.cff).
251
+
252
+ ## Academic citation
253
+
254
+ ```bibtex
255
+ @software{eminoglu_retailgravity,
256
+ author = {Eminoğlu, Yusuf},
257
+ title = {retailgravity},
258
+ year = {2026},
259
+ version = {0.1.0},
260
+ url = {https://github.com/YusufEminoglu/retailgravity_sdk}
261
+ }
262
+ ```
263
+
264
+ See also `CITATION.cff`.
@@ -0,0 +1,227 @@
1
+ # retailgravity
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/retailgravity.svg)](https://pypi.org/project/retailgravity/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/retailgravity.svg)](https://pypi.org/project/retailgravity/)
5
+ [![CI](https://github.com/YusufEminoglu/retailgravity_sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/YusufEminoglu/retailgravity_sdk/actions/workflows/ci.yml)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
+
8
+ **Spatial interaction and retail catchment models for site analysis, in Python.**
9
+
10
+ `retailgravity` collects the classic retail-geography toolbox in one small, typed
11
+ package that depends on NumPy only:
12
+
13
+ - **Huff model**: patronage probabilities, expected patronage, store sales, market and fair share.
14
+ - **MCI** (Multiplicative Competitive Interaction) with the Nakanishi & Cooper **log-centering OLS** calibration. Huff's `alpha` and `beta` are estimated the same way.
15
+ - **Reilly's law** of retail gravitation and **Converse breaking points**, located on the great circle between towns.
16
+ - **Wilson's family** of spatial interaction models: unconstrained, production-, attraction- and doubly constrained (**Furness / IPF** balancing), with power or exponential deterrence.
17
+ - **Calibration of distance decay** to observed flows by Poisson **maximum likelihood** or **mean trip length** matching, reporting log-likelihood, R² and SRMSE.
18
+ - **Trade areas**: dominant-store assignment, probability contours as point sets, primary and secondary trade areas from cumulative patronage, and gridded probability surfaces.
19
+ - **What-if scenarios**: add, remove or resize stores and measure **cannibalisation**, competitor draw and net chain gain.
20
+ - Origins and stores come from lists, **CSV** or **GeoJSON** points. Distances are supplied as a matrix or computed (haversine for lon/lat, or Euclidean).
21
+ - A `retailgravity` command-line tool.
22
+
23
+ ```bash
24
+ pip install retailgravity
25
+ ```
26
+
27
+ Requires Python 3.9 or newer and NumPy.
28
+
29
+ ---
30
+
31
+ ## Quick start (Python)
32
+
33
+ ```python
34
+ from retailgravity import read_locations, attribute_array, distance_matrix, huff
35
+
36
+ origins = read_locations("examples/origins.csv") # id, lon, lat, population
37
+ stores = read_locations("examples/stores.csv") # id, lon, lat, floor_area, chain
38
+ d = distance_matrix(origins, stores) # haversine, km
39
+
40
+ res = huff(d, attribute_array(stores, "floor_area"), attribute_array(origins, "population"),
41
+ alpha=1.0, beta=2.0, store_ids=[s.id for s in stores])
42
+ res.probabilities # (origins x stores); every row sums to 1
43
+ res.sales # expected customers per store
44
+ res.market_share # sums to 1
45
+ res.store_table() # sales, market share, fair share, share index
46
+ ```
47
+
48
+ Calibrating, delineating and testing scenarios:
49
+
50
+ ```python
51
+ from retailgravity import calibrate_beta, fit_huff, cumulative_trade_area, Market, Store, compare
52
+
53
+ # beta from observed origin-store flows (production-constrained = Huff with spending O_i)
54
+ cal = calibrate_beta(observed, d, constraint="production", deterrence_kind="power",
55
+ destination_mass=floor_area)
56
+ cal.beta, cal.log_likelihood, cal.r_squared, cal.srmse
57
+
58
+ # alpha and beta from observed shares via MCI log-centering regression
59
+ fit = fit_huff(observed_shares, d, floor_area)
60
+ fit.alpha, fit.beta, fit.mci.summary()
61
+
62
+ # primary (60 %) / secondary (85 %) trade area of store 0, nearest origins first
63
+ labels = cumulative_trade_area(res.patronage[:, 0], order_by=d[:, 0])
64
+
65
+ # a new store for chain "Alpha"
66
+ market = Market.from_coordinates(origins, population, [Store("S1", 12000, 27.07, 38.41, "Alpha"), ...])
67
+ print(compare(market, market.add_store(Store("S5", 6000, 27.15, 38.47, "Alpha"))).summary())
68
+ ```
69
+
70
+ `python examples/quickstart.py` runs all of this on the bundled synthetic data.
71
+
72
+ ## Quick start (command line)
73
+
74
+ ```bash
75
+ # Huff: store summary to stdout, long origin-store table to CSV
76
+ retailgravity huff --origins examples/origins.csv --stores examples/stores.csv \
77
+ --demand-col population --attractiveness-col floor_area --beta 2 -o huff.csv
78
+
79
+ # Trade area of S1 as GeoJSON points (dominant store, probability band, primary/secondary)
80
+ retailgravity tradearea --origins examples/origins.csv --stores examples/stores.csv \
81
+ --demand-col population --attractiveness-col floor_area --store S1 \
82
+ --levels 0.75,0.5,0.25 --cumulative 0.6,0.85 -o s1_trade_area.geojson
83
+
84
+ # What-if: Alpha opens S5; S2 is enlarged by 50 %
85
+ retailgravity scenario --origins examples/origins.csv --stores examples/stores.csv \
86
+ --demand-col population --attractiveness-col floor_area --chain-col chain \
87
+ --add S5,27.15,38.47,6000,Alpha --resize "S2*1.5"
88
+
89
+ # Breaking points between every pair of towns
90
+ retailgravity breakpoint --places towns.csv --size-col population
91
+
92
+ # Calibrate beta from a long flow table (origin, destination, flow, cost)
93
+ retailgravity calibrate --flows flows.csv --constraint doubly --deterrence exponential
94
+ ```
95
+
96
+ Output of the scenario command on the example data:
97
+
98
+ ```
99
+ store chain base scenario change %
100
+ S1 Alpha 79136.22 72927.53 -6208.69 -7.8
101
+ S2 Beta 66232.10 53157.53 -13074.57 -19.7
102
+ S3 Alpha 73996.17 62843.20 -11152.98 -15.1
103
+ S4 Gamma 57635.51 52495.37 -5140.14 -8.9
104
+ S5 Alpha 0.00 35576.38 35576.38
105
+ new store sales: 35576.38
106
+ cannibalisation (own chain): 17361.67
107
+ drawn from competitors: 18214.71
108
+ cannibalisation rate: 48.8 %
109
+ net change for chain Alpha: 18214.71
110
+ ```
111
+
112
+ Precomputed network distances or travel times can replace the straight-line
113
+ distances: pass a matrix in Python, or `--distances` with a long CSV
114
+ (`origin_id,store_id,distance`) on the command line.
115
+
116
+ ## API overview
117
+
118
+ | Area | Functions / classes |
119
+ |---|---|
120
+ | Distances | `distance_matrix`, `haversine_matrix`, `euclidean_matrix`, `interpolate` |
121
+ | Input/output | `read_locations` (CSV/GeoJSON), `locations_from_records`, `Location`, `attribute_array`, `coordinates`, `write_csv` |
122
+ | Huff | `huff` → `HuffResult`, `huff_probabilities`, `utilities`, `deterrence` |
123
+ | MCI | `mci_probabilities`, `fit_mci` → `MCIFit`, `fit_huff` → `HuffFit` |
124
+ | Reilly / Converse | `reilly_ratio`, `reilly_shares`, `breaking_point`, `breaking_point_location`, `breaking_points` |
125
+ | Wilson family | `spatial_interaction` → `SIResult`, `furness`, `mean_cost`, `ConvergenceWarning` |
126
+ | Calibration | `calibrate_beta` → `CalibrationResult`, `poisson_log_likelihood`, `goodness_of_fit` |
127
+ | Trade areas | `dominant_store`, `contour_sets`, `probability_bands`, `cumulative_trade_area`, `probability_surface` → `ProbabilitySurface` |
128
+ | Scenarios | `Market`, `Store`, `compare` → `ScenarioComparison` |
129
+
130
+ ## Methods and sources
131
+
132
+ **Huff model** (Huff 1963, 1964). `P_ij = S_j^α f(d_ij) / Σ_k S_k^α f(d_ik)` with
133
+ `f(d) = d^-β` (Huff's λ is called `beta` here) or `exp(-β d)`. Expected patronage
134
+ `E_ij = C_i P_ij`. An infinite distance takes a store out of an origin's choice set. A zero
135
+ distance with the power form needs `min_distance`.
136
+
137
+ **MCI and log-centering** (Nakanishi & Cooper 1974; Cooper & Nakanishi 1988).
138
+ `P_ij = Π_k X_kij^β_k / Σ_l Π_k X_kil^β_k`. Dividing by the geometric mean of each choice
139
+ set gives `log(p_ij/p̃_i) = Σ_k β_k log(X_kij/X̃_ki)`, which is estimated by OLS without an
140
+ intercept. This equals a regression on logs with one dummy per choice set, so the residual
141
+ degrees of freedom are `N − I − K`. The test suite checks the equivalence numerically. Zero
142
+ shares cannot be logged and are dropped.
143
+
144
+ **Reilly and Converse** (Reilly 1931; Converse 1949). `B_a/B_b = (P_a/P_b)(D_b/D_a)²`.
145
+ The breaking point measured from the smaller place B is `D_B = D_AB / (1 + √(P_A/P_B))`.
146
+ Both exponents can be changed.
147
+
148
+ **Wilson's family** (Wilson 1971; Fotheringham & O'Kelly 1989):
149
+ unconstrained `T_ij = k V_i^μ W_j^α f(c_ij)`; production-constrained
150
+ `T_ij = A_i O_i W_j^α f(c_ij)` (the retail model, identical to Huff with power deterrence);
151
+ attraction-constrained `T_ij = B_j D_j V_i^μ f(c_ij)`; doubly constrained
152
+ `T_ij = A_i O_i B_j D_j f(c_ij)`, balanced by Furness (1965) iterations until both margins
153
+ are met to a relative tolerance of 1e-9.
154
+
155
+ **Calibration** (Hyman 1969; Batty & Mackie 1972). When the balancing factors (or `k`)
156
+ are free, the Poisson ML estimate of β reproduces the observed flow-weighted mean of
157
+ `c` (exponential) or `ln c` (power). `calibrate_beta(method="mle")` solves that
158
+ monotone equation by bisection. `method="mean_trip_length"` matches the mean cost
159
+ (Hyman's criterion) and gives the same answer for exponential deterrence. Fit is reported
160
+ as the Poisson log-likelihood, R² and SRMSE (Knudsen & Fotheringham 1986).
161
+
162
+ **Trade areas.** Huff mapped trade areas as equal-probability contours.
163
+ `contour_sets` and `ProbabilitySurface.contour_points` return them as point sets.
164
+ `cumulative_trade_area` ranks origins by distance or by contribution and cuts at
165
+ cumulative shares of the store's patronage. The default 60 % / 85 % primary/secondary
166
+ cut-offs are a common rule of thumb, not a published standard. Set them for your study.
167
+
168
+ **Scenarios.** Total demand is fixed, so a new store's sales always equal the losses of
169
+ existing stores. These losses split into cannibalisation (own chain) and competitor draw.
170
+
171
+ References:
172
+
173
+ - Huff, D. L. (1963). A probabilistic analysis of shopping center trade areas. *Land Economics*, 39(1), 81–90.
174
+ - Huff, D. L. (1964). Defining and estimating a trading area. *Journal of Marketing*, 28(3), 34–38.
175
+ - Nakanishi, M., & Cooper, L. G. (1974). Parameter estimation for a multiplicative competitive interaction model: least squares approach. *Journal of Marketing Research*, 11(3), 303–311.
176
+ - Cooper, L. G., & Nakanishi, M. (1988). *Market-Share Analysis*. Kluwer.
177
+ - Reilly, W. J. (1931). *The Law of Retail Gravitation*. New York: Knickerbocker Press.
178
+ - Converse, P. D. (1949). New laws of retail gravitation. *Journal of Marketing*, 14, 379–384.
179
+ - Wilson, A. G. (1971). A family of spatial interaction models, and associated developments. *Environment and Planning A*, 3(1), 1–32.
180
+ - Furness, K. P. (1965). Time function iteration. *Traffic Engineering and Control*, 7(7), 458–460.
181
+ - Hyman, G. M. (1969). The calibration of trip distribution models. *Environment and Planning*, 1(1), 105–112.
182
+ - Batty, M., & Mackie, S. (1972). The calibration of gravity, entropy, and related models of spatial interaction. *Environment and Planning*, 4(2), 205–233.
183
+ - Fotheringham, A. S., & O'Kelly, M. E. (1989). *Spatial Interaction Models: Formulations and Applications*. Kluwer.
184
+ - Knudsen, D. C., & Fotheringham, A. S. (1986). Matrix comparison, goodness-of-fit, and spatial interaction modeling. *International Regional Science Review*, 10(2), 127–147.
185
+ - Sinnott, R. W. (1984). Virtues of the haversine. *Sky and Telescope*, 68(2), 159.
186
+
187
+ ## Limitations
188
+
189
+ - Distances are straight-line (spherical haversine, mean Earth radius 6371.0088 km, or planar Euclidean). For network distances or travel times, supply your own matrix (for example from a routing engine).
190
+ - Demand is fixed. The models redistribute spending and do not create new spending, so they do not model market expansion or outshopping beyond the study area. Choose a study area large enough to avoid edge effects.
191
+ - Huff and MCI assume independence from irrelevant alternatives (IIA). Closely competing nearby stores can be over-predicted.
192
+ - MCI calibration needs strictly positive shares and attributes. Zero shares are dropped, which biases estimates when zeros are common.
193
+ - `calibrate_beta` estimates one global β. It does not estimate local or origin-specific decay, the attractiveness exponent of constrained models, or confidence intervals.
194
+ - The 60 % / 85 % cumulative trade-area defaults are a convention and must be overridden as needed.
195
+
196
+ ## Development
197
+
198
+ ```bash
199
+ pip install -e ".[dev]"
200
+ pytest --cov=retailgravity
201
+ ruff check . && ruff format --check src tests && mypy src
202
+ ```
203
+
204
+ The tests check worked examples against hand-computed values. Examples include Huff
205
+ probabilities of 6/11, 3/11 and 2/11, a Converse breaking point of 10 miles, the Furness
206
+ closed form `O_i D_j / T`, and exact recovery of known MCI and β parameters. They also
207
+ check invariants: rows sum to 1, constrained margins are reproduced, and cannibalisation
208
+ plus competitor draw equals the new store's sales.
209
+
210
+ ## License
211
+
212
+ MIT © 2026 Yusuf Eminoğlu. See [LICENSE](LICENSE). To cite retailgravity, see
213
+ [CITATION.cff](CITATION.cff).
214
+
215
+ ## Academic citation
216
+
217
+ ```bibtex
218
+ @software{eminoglu_retailgravity,
219
+ author = {Eminoğlu, Yusuf},
220
+ title = {retailgravity},
221
+ year = {2026},
222
+ version = {0.1.0},
223
+ url = {https://github.com/YusufEminoglu/retailgravity_sdk}
224
+ }
225
+ ```
226
+
227
+ See also `CITATION.cff`.
@@ -0,0 +1,17 @@
1
+ id,lon,lat,population
2
+ N01,27.05736,38.39603,16000
3
+ N02,27.05979,38.42090,4000
4
+ N03,27.05902,38.45418,29000
5
+ N04,27.06490,38.47059,20000
6
+ N05,27.09037,38.39777,9000
7
+ N06,27.09327,38.42262,4000
8
+ N07,27.09329,38.45307,29000
9
+ N08,27.08772,38.47165,13000
10
+ N09,27.12127,38.40232,15000
11
+ N10,27.11587,38.42606,25000
12
+ N11,27.11888,38.44749,11000
13
+ N12,27.11974,38.47089,30000
14
+ N13,27.15365,38.40048,13000
15
+ N14,27.14507,38.42292,26000
16
+ N15,27.15382,38.45348,20000
17
+ N16,27.14695,38.47424,13000