geomask
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, ormethod="local") and itsforward/inversemethods to convert. populationis 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 acceptspopulation_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; publishreleased_countsortable(), 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
# (the k check keeps a safety margin that covers the --decimals rounding, and
# the k-anonymity of the coordinates actually written is reported on stderr)
# 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
populationlayer: 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.1
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 |
|---|---|---|---|---|
| geomask-0.2.1.tar.gz | 58.5 kB | With provenance | Details for geomask-0.2.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| geomask-0.2.1-py3-none-any.whl | Python 3 | none | any | Details for geomask-0.2.1-py3-none-any.whl |
Total release size: 101.8 kB
Release files / geomask-0.2.1.tar.gz
| Download URL | geomask-0.2.1.tar.gz |
|---|---|
| Size | 58.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c48cbe0becbfe36e403ef6c5de083570332bdd1070870a31d8afb6608f8cb6d8
|
|
BLAKE2b-256 checksum How to use checksums |
36fb58e9a9680933726929d014169a98f210d8cba1c328118f1e8b29d67b2a2e
|
| 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 logRelease files / geomask-0.2.1-py3-none-any.whl
| Download URL | geomask-0.2.1-py3-none-any.whl |
|---|---|
| Size | 43.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fe8da704e5f77f685b78328d1f820379516b1e747b4efc6e43a673b8109f8879
|
|
BLAKE2b-256 checksum How to use checksums |
d16f7961c7eefc7c3bca376b98b36449622e1a63bad866bec1869f0d62f6f9ea
|
| 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 logSecurity for geomask 0.2.1
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 geomask-0.2.1.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 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.