Skip to main content

geomask

CI PyPI Python License: MIT

Geographic masking of confidential point data, with the privacy and utility metrics you need to document the trade-off.

Public-health, social-science and urban researchers often hold point data that identify people: home addresses of patients, locations of incidents, survey respondents' dwellings. Publishing those points, or even a detailed map of them, can let anyone with a street map work out who is who. geomask implements the established geomasking methods from the literature and, just as importantly, the metrics that tell you how much privacy a mask buys and how much analytical value it costs.

  • Masks: random perturbation, Gaussian displacement, donut masking, population-adaptive donut masking with a verified spatial k-anonymity guarantee, grid and hexagon snapping, grid / hexagon / Voronoi / label aggregation with small-cell suppression, location swapping and adaptive areal elimination (AAE).
  • Privacy metrics: spatial k-anonymity per point, expected re-identification risk, nearest-neighbour linkage rate, displacement statistics.
  • Utility metrics: Ripley's K / L difference, kernel-density correlation, Moran's I and hot-spot overlap of gridded counts, mean displacement.
  • report(): a privacy-vs-utility table across methods.
  • Reproducible (every random mask takes a seed), pure NumPy (SciPy optional for KD-tree speed), works in metres with built-in lon/lat helpers (UTM and local equirectangular), typed, and a CLI.

Masking is not anonymisation by itself. Read the ethics and limitations section before you share anything.

Installation

pip install geomask            # NumPy only
pip install "geomask[fast]"    # + SciPy KD-trees for large data sets

Python 3.9 or later.

Quick start

All examples use synthetic data generated by geomask.synthetic_city; no real person is represented anywhere in this project.

import geomask as gm

town = gm.synthetic_city(n_population=5000, n_cases=300, seed=1)
cases, homes = town.cases, town.population   # (n, 2) arrays in metres

# Donut mask: every point moves between 50 m and 300 m.
masked = gm.donut_mask(cases, min_radius=50, max_radius=300, seed=1)

# Population-adaptive donut: radii grow in sparse areas and every masked point
# is re-drawn until at least 10 households are closer to it than the true home.
safe = gm.donut_mask(cases, 50, 300, seed=1, population=homes, k=10)

k = gm.spatial_k_anonymity(cases, safe, homes)
print(k.min())                          # >= 10, audited independently

gm.report(gm.evaluate(cases, safe, population=homes, k=10))

Comparing methods

examples/compare_methods.py masks the same synthetic cases with every method and prints the trade-off table:

results = gm.compare(cases, {
    "perturb r=250": gm.random_perturbation(cases, 250, seed=1),
    "gaussian s=100": gm.gaussian_displacement(cases, 100, seed=1),
    "donut 50-300": gm.donut_mask(cases, 50, 300, seed=1),
    "donut k>=10": gm.donut_mask(cases, 50, 300, seed=1, population=homes, k=10),
    "swap r=300": gm.location_swap(cases, homes, 300, seed=1),
    "AAE k=20": gm.adaptive_areal_elimination(cases, homes, k=20, cell_size=100, seed=1),
    "grid 250 k=5": gm.aggregate_to_grid(cases, 250, k=5).masked_points,
    "hex 150 k=5": gm.aggregate_to_grid(cases, 150, k=5, shape="hex").masked_points,
}, population=homes, k=10)
gm.report(results)
method          k min  k med   <k %   risk  NN re-id  disp m  min m  dL max m  KDE r  Moran I  hot J  suppr
--------------  -----  -----  -----  -----  --------  ------  -----  --------  -----  -------  -----  -----
perturb r=250       0     42  20.0%  0.081     19.7%     165     19      26.9  0.999    0.700   0.36      0
gaussian s=100      0     19  35.0%  0.174     34.7%     118      4      23.1  0.998    0.702   0.47      0
donut 50-300        0     62  13.7%  0.049     15.7%     203     55      37.2  0.998    0.738   0.67      0
donut k>=10        10     66   0.0%  0.023      8.7%     224     66      31.9  0.997    0.755   0.62      0
swap r=300          1     88  12.0%  0.035     13.3%     207     26      45.3  0.989    0.663   0.53      0
AAE k=20            0     24  27.0%  0.122     32.3%     238      6      45.2  0.993    0.678   0.55      0
grid 250 k=5        0     18  28.5%  0.106      6.3%      93     14     727.8  0.932    0.507   0.25    170
hex 150 k=5         0     18  30.1%  0.120      6.7%      93     21     668.0  0.945    0.538   0.33    167
n = 300; k threshold = 10; Moran I of original counts = 0.708

How to read it: only the population-adaptive donut guarantees that every point has at least k = 10 households closer to its released location than the true one (k min), while keeping the density surface almost unchanged (KDE r 0.997). Fixed-radius masks leave some rural points weakly protected (<k %). Aggregation with suppression drops more than half the points in this small sample (suppr) and distorts second-order structure (dL max). AAE's guarantee is areal (each release area holds at least k residents), which the point-based k min column does not measure - see below.

Methods

Method Function What it does Privacy Utility Reference
Random perturbation random_perturbation Uniform random point within radius r (uniform over the disk, or uniform distance) Weak: some points barely move Good at scales above r Armstrong et al. 1999
Gaussian displacement gaussian_displacement Adds N(0, sigma^2) to x and y; distance is Rayleigh Weak: unbounded small moves Good, smooth Armstrong et al. 1999; Zandbergen 2014
Donut masking donut_mask Random point in an annulus [r_min, r_max] Guaranteed minimum displacement Bounded error r_max Hampton et al. 2010
Population-adaptive donut donut_mask(population=, k=) Radii from the k-th / k_max-th nearest population unit; candidates re-drawn until spatial k-anonymity >= k Verified k per point Small moves in dense areas, larger in sparse Allshouse et al. 2010; Hampton et al. 2010
Grid / hexagon snapping snap_to_grid, snap_to_hex Moves points to cell centres Points in a cell become identical Resolution = cell size Armstrong et al. 1999
Aggregation + suppression aggregate_to_grid, aggregate_to_voronoi, aggregate_by_label Counts per cell / Voronoi cell / area; counts 0 < c < k suppressed Small-cell rule Loses point detail; suppression loses rural cases Armstrong et al. 1999
Location swapping location_swap Replaces each point with a random real address within r (never itself) Plausible locations; resists "point in a lake" checks Similar to donut Zhang et al. 2017
Adaptive areal elimination adaptive_areal_elimination Merges grid cells until each holds >= k people, then relocates points randomly inside their merged area Areal k-anonymity Adaptive: small areas in dense places Kounadi & Leitner 2016

Notes on parameters:

  • Coordinates are projected, in metres. Use gm.make_projection(lonlat) (UTM zone of the data by default, or method="local") and its forward / inverse methods to convert.
  • population is any set of locations that people could plausibly live at: address points, building centroids or the centres of a population raster. Repeat a centre once per resident to weight it (AAE also accepts population_weights). The true location is usually in this set; that is expected and handled.
  • donut_mask(..., distribution="distance") reproduces the original donut formulation (uniform distance); the default "area" is uniform over the annulus, which puts fewer points close to the inner ring.
  • Aggregation results keep the true counts in counts; publish released_counts or table(), which show suppressed cells as <k.

Metrics

Metric Function Meaning
Spatial k-anonymity spatial_k_anonymity(orig, masked, population) Number of population units strictly closer to the masked point than the true location (the true location itself is never counted, so the anonymity set has k + 1 members).
Expected re-identification risk expected_reidentification_risk(k) mean(1 / (k + 1)): probability that an adversary picking uniformly among the candidates guesses right.
Nearest-neighbour linkage rate nearest_neighbour_reidentification_rate Share of masked points whose nearest true location is their own; models an adversary holding the list of true locations.
Displacement statistics displacement_stats Mean, median, SD, min, 5th / 95th percentile and max of distances moved.
Ripley's K / L difference ripley_k, ripley_l, ripley_l_difference L(r) = sqrt(K(r) / pi) for original and masked points over a shared window; max_abs_difference in metres. No edge correction (biases both patterns equally).
Kernel-density correlation kde_correlation, kernel_density Pearson correlation of Gaussian KDE surfaces (Scott bandwidth by default).
Cluster preservation cluster_preservation, morans_i Moran's I (rook contiguity) of gridded counts before and after masking, and Jaccard overlap of hot-spot cells.
Mean displacement mean_displacement Average distance moved, metres.

gm.evaluate() computes all of them; gm.compare() evaluates several masks with identical settings; gm.report() prints the table. Suppressed points (NaN rows) are excluded from the masked pattern and counted in suppr.

Command line

# Donut mask a lon/lat CSV (coordinates are auto-detected and projected to UTM)
geomask donut cases.csv --x lon --y lat --min 50 --max 300 --seed 1 -o masked.csv

# Population-adaptive donut with k >= 10
geomask donut cases.csv --x lon --y lat --min 50 --max 300 --k 10 \
    --population addresses.csv --seed 1 -o masked.csv

# Other methods
geomask perturb  cases.csv --radius 250 -o out.csv
geomask gaussian cases.csv --sigma 100 -o out.csv
geomask grid     cases.csv --cell 250 --k 5 -o out.csv
geomask hex      cases.csv --size 150 --k 5 -o out.csv
geomask voronoi  cases.csv --seeds clinics.csv --k 5 -o out.csv
geomask swap     cases.csv --radius 300 --population addresses.csv -o out.csv
geomask aae      cases.csv --k 20 --cell 100 --population addresses.csv -o out.csv

# Privacy / utility report (rows must be in the same order)
geomask evaluate cases.csv masked.csv --x lon --y lat --population addresses.csv --k 10
geomask evaluate cases.csv masked.csv --json

All other columns are copied unchanged; use --drop name address ... to remove direct identifiers. Suppressed points are written with empty coordinates. --coords lonlat|projected overrides auto-detection and --projection local uses an equirectangular projection instead of UTM.

Reproducibility

Every random function accepts seed (an int, None or a numpy.random.Generator). The same seed, input and version give identical output. Do not publish the seed together with the masked data: with the seed and the method parameters, an adversary who knows some true locations can test candidate locations. Store the seed with your confidential project records.

Ethics and limitations

  • Masking is not anonymisation by itself. A masked point set can still be re-identified by linkage with other data (dates, ages, diagnoses, rare attributes), by repeated releases of differently masked versions of the same data, or by an adversary who knows the method and parameters. Treat masked data as reduced-risk personal data, not as anonymous data.
  • Consult your IRB / ethics committee and data-protection officer (DPO) before sharing. They decide which risk is acceptable for your data, your participants and your recipients. This package helps you document the risk; it does not approve anything.
  • GDPR note (EU/EEA and similar laws). Location data is listed as an identifier in Art. 4(1) GDPR, and health data are a special category (Art. 9). Under Recital 26, data remain personal data as long as a person is identifiable by means reasonably likely to be used; geomasked data for which you keep the original coordinates or a re-identification key are generally pseudonymised, not anonymised, and the GDPR still applies. A data-protection impact assessment (Art. 35) may be required. This is not legal advice.
  • Guarantees are conditional. Spatial k-anonymity is only as good as the population layer: missing or outdated address data overstate k. AAE's guarantee is areal (at least k residents per release area), not a guarantee about distances.
  • Small-cell suppression does not stop differencing attacks when several overlapping tables (other cell sizes, other years, sub-groups) are published, and it removes cases disproportionately from sparse, often rural or marginalised, areas - a bias to report in your methods.
  • Edge effects: points near a study-area boundary, a coastline or an uninhabited area can be displaced to implausible places (except with swapping or AAE placement="population"), which an adversary can exploit to narrow the search.
  • Utility metrics are global summaries. A high KDE correlation does not guarantee that a specific local cluster survived; re-run your actual analysis on the masked data before publishing results based on it.
  • Use only synthetic or properly authorised data when experimenting. Never commit real personal data to a code repository.

API overview

Masks         random_perturbation, gaussian_displacement, donut_mask (DonutResult),
              location_swap, snap_to_grid, snap_to_hex,
              aggregate_to_grid, aggregate_to_voronoi, aggregate_by_label (AggregationResult),
              adaptive_areal_elimination (AAEResult)
Privacy       spatial_k_anonymity, expected_reidentification_risk,
              nearest_neighbour_reidentification_rate, displacement, displacement_stats
Utility       ripley_k, ripley_l, ripley_l_difference, kernel_density, kde_correlation,
              morans_i, cluster_preservation, mean_displacement
Evaluation    evaluate (EvaluationResult), compare, report
Projection    make_projection, UTM, LocalEquirectangular, utm_zone
Data          synthetic_city (SyntheticCity)
Backend       set_backend("numpy" | "scipy" | "auto"), get_backend

References

  • Allshouse, W. B., Fitch, M. K., Hampton, K. H., Gesink, D. C., Doherty, I. A., Leone, P. A., Serre, M. L., & Miller, W. C. (2010). Geomasking sensitive health data and privacy protection: an evaluation using an E911 database. Geocarto International, 25(6), 443-452. https://doi.org/10.1080/10106049.2010.496496
  • Armstrong, M. P., Rushton, G., & Zimmerman, D. L. (1999). Geographically masking health data to preserve confidentiality. Statistics in Medicine, 18(5), 497-525.
  • Hampton, K. H., Fitch, M. K., Allshouse, W. B., Doherty, I. A., Gesink, D. C., Leone, P. A., Serre, M. L., & Miller, W. C. (2010). Mapping health data: improved privacy protection with donut method geomasking. American Journal of Epidemiology, 172(9), 1062-1069. https://doi.org/10.1093/aje/kwq248
  • Kounadi, O., & Leitner, M. (2016). Adaptive areal elimination (AAE): a transparent way of disclosing protected spatial datasets. Computers, Environment and Urban Systems, 57, 59-67. https://doi.org/10.1016/j.compenvurbsys.2016.01.004
  • Zandbergen, P. A. (2014). Ensuring confidentiality of geocoded health data: assessing geographic masking strategies for individual-level data. Advances in Medicine, 2014, 567049. https://doi.org/10.1155/2014/567049
  • Zhang, S., Freundschuh, S. M., Lenzer, K., & Zandbergen, P. A. (2017). The location swapping method for geomasking. Cartography and Geographic Information Science, 44(1), 22-34. https://doi.org/10.1080/15230406.2015.1095655

Citing

If you use geomask in research, please cite it using the metadata in CITATION.cff and cite the original method papers above.

License

MIT (c) Yusuf Eminoğlu. See LICENSE.

Metadata

Release files for geomask 0.2.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 geomask 0.2.0
File Size Uploaded Provenance Actions
geomask-0.2.0.tar.gz 55.3 kB With provenance Details for geomask-0.2.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for geomask 0.2.0
File Interpreter ABI Platform Actions
geomask-0.2.0-py3-none-any.whl Python 3 none any Details for geomask-0.2.0-py3-none-any.whl

Total release size: 97.0 kB

Release files / geomask-0.2.0.tar.gz

Download URL geomask-0.2.0.tar.gz
Size 55.3 kB
Tags Source
SHA-256 checksum
How to use checksums
bf1b40f3a429da8a187f2136f55a439e86aa72ca1562e1633d8aff5f0a13b0e0
BLAKE2b-256 checksum
How to use checksums
bfbd2debecf928a5e62d0dd6dc6f0338b6d3f8a64737b1726aa2335ca11ad771
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 8, 2026.

Transparency log

Release files / geomask-0.2.0-py3-none-any.whl

Download URL geomask-0.2.0-py3-none-any.whl
Size 41.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2b5de75f4c08c1af2eea38933b6f477efb32e7fcbd576a96fc59e075048967c0
BLAKE2b-256 checksum
How to use checksums
a2b1a7266939e1e93f6d187f07604020e9e2164953cf9a1255005a9724c04dd3
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 8, 2026.

Transparency log

Security for geomask 0.2.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 geomask-0.2.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 8, 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.

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