retailgravity
Spatial interaction and retail catchment models for site analysis, in Python.
retailgravity collects the classic retail-geography toolbox in one small, typed
package that depends on NumPy only:
- Huff model: patronage probabilities, expected patronage, store sales, market and fair share.
- MCI (Multiplicative Competitive Interaction) with the Nakanishi & Cooper log-centering OLS calibration. Huff's
alphaandbetaare estimated the same way. - Reilly's law of retail gravitation and Converse breaking points, located on the great circle between towns.
- Wilson's family of spatial interaction models: unconstrained, production-, attraction- and doubly constrained (Furness / IPF balancing), with power or exponential deterrence.
- Calibration of distance decay to observed flows by Poisson maximum likelihood or mean trip length matching, reporting log-likelihood, R² and SRMSE.
- Trade areas: dominant-store assignment, probability contours as point sets, primary and secondary trade areas from cumulative patronage, and gridded probability surfaces.
- What-if scenarios: add, remove or resize stores and measure cannibalisation, competitor draw and net chain gain.
- Origins and stores come from lists, CSV or GeoJSON points. Distances are supplied as a matrix or computed (haversine for lon/lat, or Euclidean).
- A
retailgravitycommand-line tool.
pip install retailgravity
Requires Python 3.9 or newer and NumPy.
Quick start (Python)
from retailgravity import read_locations, attribute_array, distance_matrix, huff
origins = read_locations("examples/origins.csv") # id, lon, lat, population
stores = read_locations("examples/stores.csv") # id, lon, lat, floor_area, chain
d = distance_matrix(origins, stores) # haversine, km
res = huff(d, attribute_array(stores, "floor_area"), attribute_array(origins, "population"),
alpha=1.0, beta=2.0, store_ids=[s.id for s in stores])
res.probabilities # (origins x stores); every row sums to 1
res.sales # expected customers per store
res.market_share # sums to 1
res.store_table() # sales, market share, fair share, share index
Calibrating, delineating and testing scenarios:
from retailgravity import calibrate_beta, fit_huff, cumulative_trade_area, Market, Store, compare
# beta from observed origin-store flows (production-constrained = Huff with spending O_i)
cal = calibrate_beta(observed, d, constraint="production", deterrence_kind="power",
destination_mass=floor_area)
cal.beta, cal.log_likelihood, cal.r_squared, cal.srmse
# alpha and beta from observed shares via MCI log-centering regression
fit = fit_huff(observed_shares, d, floor_area)
fit.alpha, fit.beta, fit.mci.summary()
# primary (60 %) / secondary (85 %) trade area of store 0, nearest origins first
labels = cumulative_trade_area(res.patronage[:, 0], order_by=d[:, 0])
# a new store for chain "Alpha"
market = Market.from_coordinates(origins, population, [Store("S1", 12000, 27.07, 38.41, "Alpha"), ...])
print(compare(market, market.add_store(Store("S5", 6000, 27.15, 38.47, "Alpha"))).summary())
python examples/quickstart.py runs all of this on the bundled synthetic data.
Quick start (command line)
# Huff: store summary to stdout, long origin-store table to CSV
retailgravity huff --origins examples/origins.csv --stores examples/stores.csv \
--demand-col population --attractiveness-col floor_area --beta 2 -o huff.csv
# Trade area of S1 as GeoJSON points (dominant store, probability band, primary/secondary)
retailgravity tradearea --origins examples/origins.csv --stores examples/stores.csv \
--demand-col population --attractiveness-col floor_area --store S1 \
--levels 0.75,0.5,0.25 --cumulative 0.6,0.85 -o s1_trade_area.geojson
# What-if: Alpha opens S5; S2 is enlarged by 50 %
retailgravity scenario --origins examples/origins.csv --stores examples/stores.csv \
--demand-col population --attractiveness-col floor_area --chain-col chain \
--add S5,27.15,38.47,6000,Alpha --resize "S2*1.5"
# Breaking points between every pair of towns
retailgravity breakpoint --places towns.csv --size-col population
# Calibrate beta from a long flow table (origin, destination, flow, cost)
retailgravity calibrate --flows flows.csv --constraint doubly --deterrence exponential
Output of the scenario command on the example data:
store chain base scenario change %
S1 Alpha 79136.22 72927.53 -6208.69 -7.8
S2 Beta 66232.10 53157.53 -13074.57 -19.7
S3 Alpha 73996.17 62843.20 -11152.98 -15.1
S4 Gamma 57635.51 52495.37 -5140.14 -8.9
S5 Alpha 0.00 35576.38 35576.38
new store sales: 35576.38
cannibalisation (own chain): 17361.67
drawn from competitors: 18214.71
cannibalisation rate: 48.8 %
net change for chain Alpha: 18214.71
Precomputed network distances or travel times can replace the straight-line
distances: pass a matrix in Python, or --distances with a long CSV
(origin_id,store_id,distance) on the command line.
API overview
| Area | Functions / classes |
|---|---|
| Distances | distance_matrix, haversine_matrix, euclidean_matrix, interpolate |
| Input/output | read_locations (CSV/GeoJSON), locations_from_records, Location, attribute_array, coordinates, write_csv |
| Huff | huff → HuffResult, huff_probabilities, utilities, deterrence |
| MCI | mci_probabilities, fit_mci → MCIFit, fit_huff → HuffFit |
| Reilly / Converse | reilly_ratio, reilly_shares, breaking_point, breaking_point_location, breaking_points |
| Wilson family | spatial_interaction → SIResult, furness, mean_cost, ConvergenceWarning |
| Calibration | calibrate_beta → CalibrationResult, poisson_log_likelihood, goodness_of_fit |
| Trade areas | dominant_store, contour_sets, probability_bands, cumulative_trade_area, probability_surface → ProbabilitySurface |
| Scenarios | Market, Store, compare → ScenarioComparison |
Methods and sources
Huff model (Huff 1963, 1964). P_ij = S_j^α f(d_ij) / Σ_k S_k^α f(d_ik) with
f(d) = d^-β (Huff's λ is called beta here) or exp(-β d). Expected patronage
E_ij = C_i P_ij. An infinite distance takes a store out of an origin's choice set. A zero
distance with the power form needs min_distance.
MCI and log-centering (Nakanishi & Cooper 1974; Cooper & Nakanishi 1988).
P_ij = Π_k X_kij^β_k / Σ_l Π_k X_kil^β_k. Dividing by the geometric mean of each choice
set gives log(p_ij/p̃_i) = Σ_k β_k log(X_kij/X̃_ki), which is estimated by OLS without an
intercept. This equals a regression on logs with one dummy per choice set, so the residual
degrees of freedom are N − I − K. The test suite checks the equivalence numerically. Zero
shares cannot be logged and are dropped.
Reilly and Converse (Reilly 1931; Converse 1949). B_a/B_b = (P_a/P_b)(D_b/D_a)².
The breaking point measured from the smaller place B is D_B = D_AB / (1 + √(P_A/P_B)).
Both exponents can be changed.
Wilson's family (Wilson 1971; Fotheringham & O'Kelly 1989):
unconstrained T_ij = k V_i^μ W_j^α f(c_ij); production-constrained
T_ij = A_i O_i W_j^α f(c_ij) (the retail model, identical to Huff with power deterrence);
attraction-constrained T_ij = B_j D_j V_i^μ f(c_ij); doubly constrained
T_ij = A_i O_i B_j D_j f(c_ij), balanced by Furness (1965) iterations until both margins
are met to a relative tolerance of 1e-9.
Calibration (Hyman 1969; Batty & Mackie 1972). When the balancing factors (or k)
are free, the Poisson ML estimate of β reproduces the observed flow-weighted mean of
c (exponential) or ln c (power). calibrate_beta(method="mle") solves that
monotone equation by bisection. method="mean_trip_length" matches the mean cost
(Hyman's criterion) and gives the same answer for exponential deterrence. Fit is reported
as the Poisson log-likelihood, R² and SRMSE (Knudsen & Fotheringham 1986).
Trade areas. Huff mapped trade areas as equal-probability contours.
contour_sets and ProbabilitySurface.contour_points return them as point sets.
cumulative_trade_area ranks origins by distance or by contribution and cuts at
cumulative shares of the store's patronage. The default 60 % / 85 % primary/secondary
cut-offs are a common rule of thumb, not a published standard. Set them for your study.
Scenarios. Total demand is fixed, so a new store's sales always equal the losses of existing stores. These losses split into cannibalisation (own chain) and competitor draw.
References:
- Huff, D. L. (1963). A probabilistic analysis of shopping center trade areas. Land Economics, 39(1), 81–90.
- Huff, D. L. (1964). Defining and estimating a trading area. Journal of Marketing, 28(3), 34–38.
- 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.
- Cooper, L. G., & Nakanishi, M. (1988). Market-Share Analysis. Kluwer.
- Reilly, W. J. (1931). The Law of Retail Gravitation. New York: Knickerbocker Press.
- Converse, P. D. (1949). New laws of retail gravitation. Journal of Marketing, 14, 379–384.
- Wilson, A. G. (1971). A family of spatial interaction models, and associated developments. Environment and Planning A, 3(1), 1–32.
- Furness, K. P. (1965). Time function iteration. Traffic Engineering and Control, 7(7), 458–460.
- Hyman, G. M. (1969). The calibration of trip distribution models. Environment and Planning, 1(1), 105–112.
- Batty, M., & Mackie, S. (1972). The calibration of gravity, entropy, and related models of spatial interaction. Environment and Planning, 4(2), 205–233.
- Fotheringham, A. S., & O'Kelly, M. E. (1989). Spatial Interaction Models: Formulations and Applications. Kluwer.
- Knudsen, D. C., & Fotheringham, A. S. (1986). Matrix comparison, goodness-of-fit, and spatial interaction modeling. International Regional Science Review, 10(2), 127–147.
- Sinnott, R. W. (1984). Virtues of the haversine. Sky and Telescope, 68(2), 159.
Limitations
- 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).
- 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.
- Huff and MCI assume independence from irrelevant alternatives (IIA). Closely competing nearby stores can be over-predicted.
- MCI calibration needs strictly positive shares and attributes. Zero shares are dropped, which biases estimates when zeros are common.
calibrate_betaestimates one global β. It does not estimate local or origin-specific decay, the attractiveness exponent of constrained models, or confidence intervals.- The 60 % / 85 % cumulative trade-area defaults are a convention and must be overridden as needed.
Development
pip install -e ".[dev]"
pytest --cov=retailgravity
ruff check . && ruff format --check src tests && mypy src
The tests check worked examples against hand-computed values. Examples include Huff
probabilities of 6/11, 3/11 and 2/11, a Converse breaking point of 10 miles, the Furness
closed form O_i D_j / T, and exact recovery of known MCI and β parameters. They also
check invariants: rows sum to 1, constrained margins are reproduced, and cannibalisation
plus competitor draw equals the new store's sales.
License
MIT © 2026 Yusuf Eminoğlu. See LICENSE. To cite retailgravity, see CITATION.cff.
Academic citation
@software{eminoglu_retailgravity,
author = {Eminoğlu, Yusuf},
title = {retailgravity},
year = {2026},
version = {0.1.0},
url = {https://github.com/YusufEminoglu/retailgravity_sdk}
}
See also CITATION.cff.
Metadata
Release files for retailgravity 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| retailgravity-0.1.0.tar.gz | 57.4 kB | With provenance | Details for retailgravity-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| retailgravity-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for retailgravity-0.1.0-py3-none-any.whl |
Total release size: 100.7 kB
Release files / retailgravity-0.1.0.tar.gz
| Download URL | retailgravity-0.1.0.tar.gz |
|---|---|
| Size | 57.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1f41f174b1381c19425b6d42b78d5f97147dba823ee567ad906ba774a60b8a73
|
|
BLAKE2b-256 checksum How to use checksums |
8afe9a90c88e2446ab632f13eeeb83e3e6f0e4252707eb034477a48c2a9cb9a1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 9, 2026.
Transparency logRelease files / retailgravity-0.1.0-py3-none-any.whl
| Download URL | retailgravity-0.1.0-py3-none-any.whl |
|---|---|
| Size | 43.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
38ba609bd48af2955e01d86360aab4b7c3c7fe0145aefc7623209db594825410
|
|
BLAKE2b-256 checksum How to use checksums |
5ba27df3d804bd9c742f9cedf9220272f01ba1f002b09a749227c30d234ca77a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 9, 2026.
Transparency logSecurity for retailgravity 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for retailgravity-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 9, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.