Skip to main content

retailgravity

PyPI version Python versions CI License: MIT

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 alpha and beta are 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 retailgravity command-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_beta estimates 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)

Source distribution for retailgravity 0.1.0
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)

Table of built distributions (wheels) for retailgravity 0.1.0
File Interpreter ABI Platform Actions
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 log

Release 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 log

Security 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.

Select from 2 files

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 log

A 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.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page