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.
- retailgravity-0.1.0/CHANGELOG.md +28 -0
- retailgravity-0.1.0/CITATION.cff +22 -0
- retailgravity-0.1.0/LICENSE +21 -0
- retailgravity-0.1.0/MANIFEST.in +9 -0
- retailgravity-0.1.0/PKG-INFO +264 -0
- retailgravity-0.1.0/README.md +227 -0
- retailgravity-0.1.0/examples/origins.csv +17 -0
- retailgravity-0.1.0/examples/quickstart.py +80 -0
- retailgravity-0.1.0/examples/stores.csv +5 -0
- retailgravity-0.1.0/pyproject.toml +114 -0
- retailgravity-0.1.0/setup.cfg +4 -0
- retailgravity-0.1.0/src/retailgravity/__init__.py +120 -0
- retailgravity-0.1.0/src/retailgravity/__main__.py +7 -0
- retailgravity-0.1.0/src/retailgravity/_arrays.py +80 -0
- retailgravity-0.1.0/src/retailgravity/_version.py +1 -0
- retailgravity-0.1.0/src/retailgravity/calibrate.py +279 -0
- retailgravity-0.1.0/src/retailgravity/cli.py +349 -0
- retailgravity-0.1.0/src/retailgravity/distance.py +132 -0
- retailgravity-0.1.0/src/retailgravity/huff.py +243 -0
- retailgravity-0.1.0/src/retailgravity/io.py +207 -0
- retailgravity-0.1.0/src/retailgravity/mci.py +237 -0
- retailgravity-0.1.0/src/retailgravity/py.typed +0 -0
- retailgravity-0.1.0/src/retailgravity/reilly.py +188 -0
- retailgravity-0.1.0/src/retailgravity/scenario.py +304 -0
- retailgravity-0.1.0/src/retailgravity/tradearea.py +190 -0
- retailgravity-0.1.0/src/retailgravity/wilson.py +283 -0
- retailgravity-0.1.0/src/retailgravity.egg-info/PKG-INFO +264 -0
- retailgravity-0.1.0/src/retailgravity.egg-info/SOURCES.txt +40 -0
- retailgravity-0.1.0/src/retailgravity.egg-info/dependency_links.txt +1 -0
- retailgravity-0.1.0/src/retailgravity.egg-info/entry_points.txt +2 -0
- retailgravity-0.1.0/src/retailgravity.egg-info/requires.txt +7 -0
- retailgravity-0.1.0/src/retailgravity.egg-info/top_level.txt +1 -0
- retailgravity-0.1.0/tests/test_calibrate.py +149 -0
- retailgravity-0.1.0/tests/test_cli.py +176 -0
- retailgravity-0.1.0/tests/test_distance.py +75 -0
- retailgravity-0.1.0/tests/test_huff.py +112 -0
- retailgravity-0.1.0/tests/test_io.py +112 -0
- retailgravity-0.1.0/tests/test_mci.py +139 -0
- retailgravity-0.1.0/tests/test_reilly.py +77 -0
- retailgravity-0.1.0/tests/test_scenario.py +117 -0
- retailgravity-0.1.0/tests/test_tradearea.py +111 -0
- 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,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
|
+
[](https://pypi.org/project/retailgravity/)
|
|
41
|
+
[](https://pypi.org/project/retailgravity/)
|
|
42
|
+
[](https://github.com/YusufEminoglu/retailgravity_sdk/actions/workflows/ci.yml)
|
|
43
|
+
[](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
|
+
[](https://pypi.org/project/retailgravity/)
|
|
4
|
+
[](https://pypi.org/project/retailgravity/)
|
|
5
|
+
[](https://github.com/YusufEminoglu/retailgravity_sdk/actions/workflows/ci.yml)
|
|
6
|
+
[](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
|