geomask 0.2.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.
- geomask-0.2.0/CHANGELOG.md +44 -0
- geomask-0.2.0/CITATION.cff +20 -0
- geomask-0.2.0/LICENSE +21 -0
- geomask-0.2.0/MANIFEST.in +9 -0
- geomask-0.2.0/PKG-INFO +325 -0
- geomask-0.2.0/README.md +285 -0
- geomask-0.2.0/examples/compare_methods.py +30 -0
- geomask-0.2.0/pyproject.toml +122 -0
- geomask-0.2.0/setup.cfg +4 -0
- geomask-0.2.0/src/geomask/__init__.py +103 -0
- geomask-0.2.0/src/geomask/_spatial.py +178 -0
- geomask-0.2.0/src/geomask/_utils.py +77 -0
- geomask-0.2.0/src/geomask/aae.py +214 -0
- geomask-0.2.0/src/geomask/aggregation.py +183 -0
- geomask-0.2.0/src/geomask/cli.py +236 -0
- geomask-0.2.0/src/geomask/evaluate.py +205 -0
- geomask-0.2.0/src/geomask/io.py +80 -0
- geomask-0.2.0/src/geomask/masking.py +336 -0
- geomask-0.2.0/src/geomask/privacy.py +127 -0
- geomask-0.2.0/src/geomask/projection.py +223 -0
- geomask-0.2.0/src/geomask/py.typed +0 -0
- geomask-0.2.0/src/geomask/synthetic.py +65 -0
- geomask-0.2.0/src/geomask/utility.py +274 -0
- geomask-0.2.0/src/geomask.egg-info/PKG-INFO +325 -0
- geomask-0.2.0/src/geomask.egg-info/SOURCES.txt +35 -0
- geomask-0.2.0/src/geomask.egg-info/dependency_links.txt +1 -0
- geomask-0.2.0/src/geomask.egg-info/entry_points.txt +2 -0
- geomask-0.2.0/src/geomask.egg-info/requires.txt +10 -0
- geomask-0.2.0/src/geomask.egg-info/top_level.txt +1 -0
- geomask-0.2.0/tests/conftest.py +38 -0
- geomask-0.2.0/tests/test_aggregation_aae.py +145 -0
- geomask-0.2.0/tests/test_cli_io.py +189 -0
- geomask-0.2.0/tests/test_evaluate.py +65 -0
- geomask-0.2.0/tests/test_masking.py +246 -0
- geomask-0.2.0/tests/test_metrics.py +164 -0
- geomask-0.2.0/tests/test_projection.py +96 -0
- geomask-0.2.0/tests/test_spatial.py +97 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.2.0] - 2026-11-09
|
|
11
|
+
### Changed
|
|
12
|
+
- First release published on PyPI. The `geomask` name belonged to an earlier, deleted project whose
|
|
13
|
+
0.1.x file names PyPI does not allow to be reused, so 0.1.0 and 0.1.1 were never published.
|
|
14
|
+
The code is identical to 0.1.0.
|
|
15
|
+
|
|
16
|
+
## [0.1.0] - 2026-11-08
|
|
17
|
+
### Added
|
|
18
|
+
- **Masks**: `random_perturbation` (uniform disk or uniform distance),
|
|
19
|
+
`gaussian_displacement`, `donut_mask` with optional population-adaptive radii
|
|
20
|
+
and a verified spatial k-anonymity guarantee (`DonutResult`), `location_swap`,
|
|
21
|
+
`snap_to_grid`, `snap_to_hex`.
|
|
22
|
+
- **Aggregation with small-cell suppression**: `aggregate_to_grid` (square or
|
|
23
|
+
hexagon), `aggregate_to_voronoi`, `aggregate_by_label` (`AggregationResult`
|
|
24
|
+
with `released_counts` and `table()`).
|
|
25
|
+
- **Adaptive areal elimination** (Kounadi & Leitner 2016) on a regular grid:
|
|
26
|
+
`adaptive_areal_elimination` (`AAEResult`), uniform or population placement,
|
|
27
|
+
optional population weights.
|
|
28
|
+
- **Privacy metrics**: `spatial_k_anonymity`, `expected_reidentification_risk`,
|
|
29
|
+
`nearest_neighbour_reidentification_rate`, `displacement`,
|
|
30
|
+
`displacement_stats`.
|
|
31
|
+
- **Utility metrics**: `ripley_k`, `ripley_l`, `ripley_l_difference`,
|
|
32
|
+
`kernel_density`, `kde_correlation`, `morans_i`, `cluster_preservation`,
|
|
33
|
+
`mean_displacement`.
|
|
34
|
+
- `evaluate`, `compare` and `report` for a privacy-vs-utility table.
|
|
35
|
+
- Projection helpers: `UTM` (WGS84, Kruger series), `LocalEquirectangular`,
|
|
36
|
+
`make_projection`, `utm_zone`.
|
|
37
|
+
- `synthetic_city` for examples and tests without real data.
|
|
38
|
+
- `geomask` command-line tool with `perturb`, `gaussian`, `donut`, `grid`,
|
|
39
|
+
`hex`, `voronoi`, `swap`, `aae` and `evaluate` sub-commands.
|
|
40
|
+
- Optional SciPy KD-tree backend (`pip install "geomask[fast]"`) with an
|
|
41
|
+
identical pure-NumPy fallback.
|
|
42
|
+
|
|
43
|
+
[Unreleased]: https://github.com/YusufEminoglu/geomask_sdk/compare/v0.1.0...HEAD
|
|
44
|
+
[0.1.0]: https://github.com/YusufEminoglu/geomask_sdk/releases/tag/v0.1.0
|
|
@@ -0,0 +1,20 @@
|
|
|
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: "yusufeminoglu@gmail.com"
|
|
7
|
+
orcid: "https://orcid.org/0009-0005-6000-2934"
|
|
8
|
+
title: "geomask: geographic masking of confidential point data"
|
|
9
|
+
version: "0.2.0"
|
|
10
|
+
date-released: "2026-11-08"
|
|
11
|
+
license: "MIT"
|
|
12
|
+
repository-code: "https://github.com/YusufEminoglu/geomask_sdk"
|
|
13
|
+
url: "https://pypi.org/project/geomask/"
|
|
14
|
+
abstract: "Geographic masking of confidential point data (random perturbation, Gaussian displacement, donut and population-adaptive donut masking, grid/hexagon/Voronoi aggregation with small-cell suppression, location swapping, adaptive areal elimination) with spatial k-anonymity, re-identification risk and spatial-utility metrics."
|
|
15
|
+
keywords:
|
|
16
|
+
- geographic masking
|
|
17
|
+
- privacy
|
|
18
|
+
- k-anonymity
|
|
19
|
+
- public health
|
|
20
|
+
- GIS
|
geomask-0.2.0/LICENSE
ADDED
|
@@ -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.
|
geomask-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: geomask
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Geographic masking of confidential point data (donut, adaptive donut, AAE, swapping, aggregation) with spatial k-anonymity and utility metrics.
|
|
5
|
+
Author-email: Yusuf Eminoğlu <yusufeminoglu@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/YusufEminoglu/geomask_sdk
|
|
8
|
+
Project-URL: Repository, https://github.com/YusufEminoglu/geomask_sdk
|
|
9
|
+
Project-URL: Issues, https://github.com/YusufEminoglu/geomask_sdk/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/YusufEminoglu/geomask_sdk/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: geographic-masking,geomasking,privacy,confidentiality,k-anonymity,spatial-k-anonymity,donut-masking,adaptive-areal-elimination,location-swapping,small-cell-suppression,public-health,epidemiology,gis,point-pattern,ripley,statistical-disclosure-control
|
|
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: Topic :: Scientific/Engineering :: GIS
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
|
|
22
|
+
Classifier: Topic :: Security
|
|
23
|
+
Classifier: Topic :: Sociology
|
|
24
|
+
Classifier: Intended Audience :: Science/Research
|
|
25
|
+
Classifier: Intended Audience :: Healthcare Industry
|
|
26
|
+
Classifier: Intended Audience :: Developers
|
|
27
|
+
Classifier: Typing :: Typed
|
|
28
|
+
Requires-Python: >=3.9
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Requires-Dist: numpy>=1.22.0
|
|
32
|
+
Provides-Extra: fast
|
|
33
|
+
Requires-Dist: scipy>=1.8.0; extra == "fast"
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
36
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
37
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
38
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
39
|
+
Dynamic: license-file
|
|
40
|
+
|
|
41
|
+
# geomask
|
|
42
|
+
|
|
43
|
+
[](https://github.com/YusufEminoglu/geomask_sdk/actions/workflows/ci.yml)
|
|
44
|
+
[](https://pypi.org/project/geomask/)
|
|
45
|
+
[](https://pypi.org/project/geomask/)
|
|
46
|
+
[](LICENSE)
|
|
47
|
+
|
|
48
|
+
**Geographic masking of confidential point data, with the privacy and utility
|
|
49
|
+
metrics you need to document the trade-off.**
|
|
50
|
+
|
|
51
|
+
Public-health, social-science and urban researchers often hold point data that
|
|
52
|
+
identify people: home addresses of patients, locations of incidents, survey
|
|
53
|
+
respondents' dwellings. Publishing those points, or even a detailed map of
|
|
54
|
+
them, can let anyone with a street map work out who is who. `geomask`
|
|
55
|
+
implements the established *geomasking* methods from the literature and,
|
|
56
|
+
just as importantly, the metrics that tell you how much privacy a mask buys
|
|
57
|
+
and how much analytical value it costs.
|
|
58
|
+
|
|
59
|
+
* **Masks**: random perturbation, Gaussian displacement, donut masking,
|
|
60
|
+
population-adaptive donut masking with a verified spatial k-anonymity
|
|
61
|
+
guarantee, grid and hexagon snapping, grid / hexagon / Voronoi / label
|
|
62
|
+
aggregation with small-cell suppression, location swapping and adaptive
|
|
63
|
+
areal elimination (AAE).
|
|
64
|
+
* **Privacy metrics**: spatial k-anonymity per point, expected
|
|
65
|
+
re-identification risk, nearest-neighbour linkage rate, displacement
|
|
66
|
+
statistics.
|
|
67
|
+
* **Utility metrics**: Ripley's K / L difference, kernel-density correlation,
|
|
68
|
+
Moran's I and hot-spot overlap of gridded counts, mean displacement.
|
|
69
|
+
* **`report()`**: a privacy-vs-utility table across methods.
|
|
70
|
+
* Reproducible (every random mask takes a `seed`), pure NumPy (SciPy optional
|
|
71
|
+
for KD-tree speed), works in metres with built-in lon/lat helpers (UTM and
|
|
72
|
+
local equirectangular), typed, and a CLI.
|
|
73
|
+
|
|
74
|
+
> **Masking is not anonymisation by itself.** Read the
|
|
75
|
+
> [ethics and limitations](#ethics-and-limitations) section before you share
|
|
76
|
+
> anything.
|
|
77
|
+
|
|
78
|
+
## Installation
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
pip install geomask # NumPy only
|
|
82
|
+
pip install "geomask[fast]" # + SciPy KD-trees for large data sets
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Python 3.9 or later.
|
|
86
|
+
|
|
87
|
+
## Quick start
|
|
88
|
+
|
|
89
|
+
All examples use **synthetic** data generated by `geomask.synthetic_city`; no
|
|
90
|
+
real person is represented anywhere in this project.
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
import geomask as gm
|
|
94
|
+
|
|
95
|
+
town = gm.synthetic_city(n_population=5000, n_cases=300, seed=1)
|
|
96
|
+
cases, homes = town.cases, town.population # (n, 2) arrays in metres
|
|
97
|
+
|
|
98
|
+
# Donut mask: every point moves between 50 m and 300 m.
|
|
99
|
+
masked = gm.donut_mask(cases, min_radius=50, max_radius=300, seed=1)
|
|
100
|
+
|
|
101
|
+
# Population-adaptive donut: radii grow in sparse areas and every masked point
|
|
102
|
+
# is re-drawn until at least 10 households are closer to it than the true home.
|
|
103
|
+
safe = gm.donut_mask(cases, 50, 300, seed=1, population=homes, k=10)
|
|
104
|
+
|
|
105
|
+
k = gm.spatial_k_anonymity(cases, safe, homes)
|
|
106
|
+
print(k.min()) # >= 10, audited independently
|
|
107
|
+
|
|
108
|
+
gm.report(gm.evaluate(cases, safe, population=homes, k=10))
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Comparing methods
|
|
112
|
+
|
|
113
|
+
`examples/compare_methods.py` masks the same synthetic cases with every
|
|
114
|
+
method and prints the trade-off table:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
results = gm.compare(cases, {
|
|
118
|
+
"perturb r=250": gm.random_perturbation(cases, 250, seed=1),
|
|
119
|
+
"gaussian s=100": gm.gaussian_displacement(cases, 100, seed=1),
|
|
120
|
+
"donut 50-300": gm.donut_mask(cases, 50, 300, seed=1),
|
|
121
|
+
"donut k>=10": gm.donut_mask(cases, 50, 300, seed=1, population=homes, k=10),
|
|
122
|
+
"swap r=300": gm.location_swap(cases, homes, 300, seed=1),
|
|
123
|
+
"AAE k=20": gm.adaptive_areal_elimination(cases, homes, k=20, cell_size=100, seed=1),
|
|
124
|
+
"grid 250 k=5": gm.aggregate_to_grid(cases, 250, k=5).masked_points,
|
|
125
|
+
"hex 150 k=5": gm.aggregate_to_grid(cases, 150, k=5, shape="hex").masked_points,
|
|
126
|
+
}, population=homes, k=10)
|
|
127
|
+
gm.report(results)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
```text
|
|
131
|
+
method k min k med <k % risk NN re-id disp m min m dL max m KDE r Moran I hot J suppr
|
|
132
|
+
-------------- ----- ----- ----- ----- -------- ------ ----- -------- ----- ------- ----- -----
|
|
133
|
+
perturb r=250 0 42 20.0% 0.081 19.7% 165 19 26.9 0.999 0.700 0.36 0
|
|
134
|
+
gaussian s=100 0 19 35.0% 0.174 34.7% 118 4 23.1 0.998 0.702 0.47 0
|
|
135
|
+
donut 50-300 0 62 13.7% 0.049 15.7% 203 55 37.2 0.998 0.738 0.67 0
|
|
136
|
+
donut k>=10 10 66 0.0% 0.023 8.7% 224 66 31.9 0.997 0.755 0.62 0
|
|
137
|
+
swap r=300 1 88 12.0% 0.035 13.3% 207 26 45.3 0.989 0.663 0.53 0
|
|
138
|
+
AAE k=20 0 24 27.0% 0.122 32.3% 238 6 45.2 0.993 0.678 0.55 0
|
|
139
|
+
grid 250 k=5 0 18 28.5% 0.106 6.3% 93 14 727.8 0.932 0.507 0.25 170
|
|
140
|
+
hex 150 k=5 0 18 30.1% 0.120 6.7% 93 21 668.0 0.945 0.538 0.33 167
|
|
141
|
+
n = 300; k threshold = 10; Moran I of original counts = 0.708
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
How to read it: only the population-adaptive donut guarantees that *every*
|
|
145
|
+
point has at least k = 10 households closer to its released location than the
|
|
146
|
+
true one (`k min`), while keeping the density surface almost unchanged
|
|
147
|
+
(`KDE r` 0.997). Fixed-radius masks leave some rural points weakly protected
|
|
148
|
+
(`<k %`). Aggregation with suppression drops more than half the points in
|
|
149
|
+
this small sample (`suppr`) and distorts second-order structure (`dL max`).
|
|
150
|
+
AAE's guarantee is *areal* (each release area holds at least k residents),
|
|
151
|
+
which the point-based `k min` column does not measure - see below.
|
|
152
|
+
|
|
153
|
+
## Methods
|
|
154
|
+
|
|
155
|
+
| Method | Function | What it does | Privacy | Utility | Reference |
|
|
156
|
+
|---|---|---|---|---|---|
|
|
157
|
+
| 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 |
|
|
158
|
+
| 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 |
|
|
159
|
+
| Donut masking | `donut_mask` | Random point in an annulus [r_min, r_max] | Guaranteed minimum displacement | Bounded error r_max | Hampton et al. 2010 |
|
|
160
|
+
| 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 |
|
|
161
|
+
| 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 |
|
|
162
|
+
| 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 |
|
|
163
|
+
| 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 |
|
|
164
|
+
| 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 |
|
|
165
|
+
|
|
166
|
+
Notes on parameters:
|
|
167
|
+
|
|
168
|
+
* Coordinates are **projected, in metres**. Use `gm.make_projection(lonlat)`
|
|
169
|
+
(UTM zone of the data by default, or `method="local"`) and its
|
|
170
|
+
`forward` / `inverse` methods to convert.
|
|
171
|
+
* `population` is any set of locations that people could plausibly live at:
|
|
172
|
+
address points, building centroids or the centres of a population raster.
|
|
173
|
+
Repeat a centre once per resident to weight it (AAE also accepts
|
|
174
|
+
`population_weights`). The true location is usually in this set; that is
|
|
175
|
+
expected and handled.
|
|
176
|
+
* `donut_mask(..., distribution="distance")` reproduces the original donut
|
|
177
|
+
formulation (uniform distance); the default `"area"` is uniform over the
|
|
178
|
+
annulus, which puts fewer points close to the inner ring.
|
|
179
|
+
* Aggregation results keep the *true* counts in `counts`; publish
|
|
180
|
+
`released_counts` or `table()`, which show suppressed cells as `<k`.
|
|
181
|
+
|
|
182
|
+
## Metrics
|
|
183
|
+
|
|
184
|
+
| Metric | Function | Meaning |
|
|
185
|
+
|---|---|---|
|
|
186
|
+
| 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). |
|
|
187
|
+
| Expected re-identification risk | `expected_reidentification_risk(k)` | mean(1 / (k + 1)): probability that an adversary picking uniformly among the candidates guesses right. |
|
|
188
|
+
| 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. |
|
|
189
|
+
| Displacement statistics | `displacement_stats` | Mean, median, SD, min, 5th / 95th percentile and max of distances moved. |
|
|
190
|
+
| 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). |
|
|
191
|
+
| Kernel-density correlation | `kde_correlation`, `kernel_density` | Pearson correlation of Gaussian KDE surfaces (Scott bandwidth by default). |
|
|
192
|
+
| 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. |
|
|
193
|
+
| Mean displacement | `mean_displacement` | Average distance moved, metres. |
|
|
194
|
+
|
|
195
|
+
`gm.evaluate()` computes all of them; `gm.compare()` evaluates several masks
|
|
196
|
+
with identical settings; `gm.report()` prints the table. Suppressed points
|
|
197
|
+
(`NaN` rows) are excluded from the masked pattern and counted in `suppr`.
|
|
198
|
+
|
|
199
|
+
## Command line
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
# Donut mask a lon/lat CSV (coordinates are auto-detected and projected to UTM)
|
|
203
|
+
geomask donut cases.csv --x lon --y lat --min 50 --max 300 --seed 1 -o masked.csv
|
|
204
|
+
|
|
205
|
+
# Population-adaptive donut with k >= 10
|
|
206
|
+
geomask donut cases.csv --x lon --y lat --min 50 --max 300 --k 10 \
|
|
207
|
+
--population addresses.csv --seed 1 -o masked.csv
|
|
208
|
+
|
|
209
|
+
# Other methods
|
|
210
|
+
geomask perturb cases.csv --radius 250 -o out.csv
|
|
211
|
+
geomask gaussian cases.csv --sigma 100 -o out.csv
|
|
212
|
+
geomask grid cases.csv --cell 250 --k 5 -o out.csv
|
|
213
|
+
geomask hex cases.csv --size 150 --k 5 -o out.csv
|
|
214
|
+
geomask voronoi cases.csv --seeds clinics.csv --k 5 -o out.csv
|
|
215
|
+
geomask swap cases.csv --radius 300 --population addresses.csv -o out.csv
|
|
216
|
+
geomask aae cases.csv --k 20 --cell 100 --population addresses.csv -o out.csv
|
|
217
|
+
|
|
218
|
+
# Privacy / utility report (rows must be in the same order)
|
|
219
|
+
geomask evaluate cases.csv masked.csv --x lon --y lat --population addresses.csv --k 10
|
|
220
|
+
geomask evaluate cases.csv masked.csv --json
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
All other columns are copied unchanged; use `--drop name address ...` to
|
|
224
|
+
remove direct identifiers. Suppressed points are written with empty
|
|
225
|
+
coordinates. `--coords lonlat|projected` overrides auto-detection and
|
|
226
|
+
`--projection local` uses an equirectangular projection instead of UTM.
|
|
227
|
+
|
|
228
|
+
## Reproducibility
|
|
229
|
+
|
|
230
|
+
Every random function accepts `seed` (an `int`, `None` or a
|
|
231
|
+
`numpy.random.Generator`). The same seed, input and version give identical
|
|
232
|
+
output. **Do not publish the seed** together with the masked data: with the
|
|
233
|
+
seed and the method parameters, an adversary who knows some true locations
|
|
234
|
+
can test candidate locations. Store the seed with your confidential project
|
|
235
|
+
records.
|
|
236
|
+
|
|
237
|
+
## Ethics and limitations
|
|
238
|
+
|
|
239
|
+
* **Masking is not anonymisation by itself.** A masked point set can still be
|
|
240
|
+
re-identified by linkage with other data (dates, ages, diagnoses, rare
|
|
241
|
+
attributes), by repeated releases of differently masked versions of the
|
|
242
|
+
same data, or by an adversary who knows the method and parameters. Treat
|
|
243
|
+
masked data as *reduced-risk* personal data, not as anonymous data.
|
|
244
|
+
* **Consult your IRB / ethics committee and data-protection officer (DPO)**
|
|
245
|
+
before sharing. They decide which risk is acceptable for your data, your
|
|
246
|
+
participants and your recipients. This package helps you document the
|
|
247
|
+
risk; it does not approve anything.
|
|
248
|
+
* **GDPR note (EU/EEA and similar laws).** Location data is listed as an
|
|
249
|
+
identifier in Art. 4(1) GDPR, and health data are a special category
|
|
250
|
+
(Art. 9). Under Recital 26, data remain personal data as long as a person
|
|
251
|
+
is identifiable by means reasonably likely to be used; geomasked data for
|
|
252
|
+
which you keep the original coordinates or a re-identification key are
|
|
253
|
+
generally *pseudonymised*, not anonymised, and the GDPR still applies. A
|
|
254
|
+
data-protection impact assessment (Art. 35) may be required. This is not
|
|
255
|
+
legal advice.
|
|
256
|
+
* **Guarantees are conditional.** Spatial k-anonymity is only as good as the
|
|
257
|
+
`population` layer: missing or outdated address data overstate k. AAE's
|
|
258
|
+
guarantee is areal (at least k residents per release area), not a
|
|
259
|
+
guarantee about distances.
|
|
260
|
+
* **Small-cell suppression** does not stop differencing attacks when several
|
|
261
|
+
overlapping tables (other cell sizes, other years, sub-groups) are
|
|
262
|
+
published, and it removes cases disproportionately from sparse, often
|
|
263
|
+
rural or marginalised, areas - a bias to report in your methods.
|
|
264
|
+
* **Edge effects**: points near a study-area boundary, a coastline or an
|
|
265
|
+
uninhabited area can be displaced to implausible places (except with
|
|
266
|
+
swapping or AAE `placement="population"`), which an adversary can exploit
|
|
267
|
+
to narrow the search.
|
|
268
|
+
* **Utility metrics are global summaries.** A high KDE correlation does not
|
|
269
|
+
guarantee that a specific local cluster survived; re-run your actual
|
|
270
|
+
analysis on the masked data before publishing results based on it.
|
|
271
|
+
* Use only synthetic or properly authorised data when experimenting. Never
|
|
272
|
+
commit real personal data to a code repository.
|
|
273
|
+
|
|
274
|
+
## API overview
|
|
275
|
+
|
|
276
|
+
```text
|
|
277
|
+
Masks random_perturbation, gaussian_displacement, donut_mask (DonutResult),
|
|
278
|
+
location_swap, snap_to_grid, snap_to_hex,
|
|
279
|
+
aggregate_to_grid, aggregate_to_voronoi, aggregate_by_label (AggregationResult),
|
|
280
|
+
adaptive_areal_elimination (AAEResult)
|
|
281
|
+
Privacy spatial_k_anonymity, expected_reidentification_risk,
|
|
282
|
+
nearest_neighbour_reidentification_rate, displacement, displacement_stats
|
|
283
|
+
Utility ripley_k, ripley_l, ripley_l_difference, kernel_density, kde_correlation,
|
|
284
|
+
morans_i, cluster_preservation, mean_displacement
|
|
285
|
+
Evaluation evaluate (EvaluationResult), compare, report
|
|
286
|
+
Projection make_projection, UTM, LocalEquirectangular, utm_zone
|
|
287
|
+
Data synthetic_city (SyntheticCity)
|
|
288
|
+
Backend set_backend("numpy" | "scipy" | "auto"), get_backend
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## References
|
|
292
|
+
|
|
293
|
+
* Allshouse, W. B., Fitch, M. K., Hampton, K. H., Gesink, D. C., Doherty,
|
|
294
|
+
I. A., Leone, P. A., Serre, M. L., & Miller, W. C. (2010). Geomasking
|
|
295
|
+
sensitive health data and privacy protection: an evaluation using an E911
|
|
296
|
+
database. *Geocarto International*, 25(6), 443-452.
|
|
297
|
+
https://doi.org/10.1080/10106049.2010.496496
|
|
298
|
+
* Armstrong, M. P., Rushton, G., & Zimmerman, D. L. (1999). Geographically
|
|
299
|
+
masking health data to preserve confidentiality. *Statistics in Medicine*,
|
|
300
|
+
18(5), 497-525.
|
|
301
|
+
* Hampton, K. H., Fitch, M. K., Allshouse, W. B., Doherty, I. A., Gesink,
|
|
302
|
+
D. C., Leone, P. A., Serre, M. L., & Miller, W. C. (2010). Mapping health
|
|
303
|
+
data: improved privacy protection with donut method geomasking. *American
|
|
304
|
+
Journal of Epidemiology*, 172(9), 1062-1069.
|
|
305
|
+
https://doi.org/10.1093/aje/kwq248
|
|
306
|
+
* Kounadi, O., & Leitner, M. (2016). Adaptive areal elimination (AAE): a
|
|
307
|
+
transparent way of disclosing protected spatial datasets. *Computers,
|
|
308
|
+
Environment and Urban Systems*, 57, 59-67.
|
|
309
|
+
https://doi.org/10.1016/j.compenvurbsys.2016.01.004
|
|
310
|
+
* Zandbergen, P. A. (2014). Ensuring confidentiality of geocoded health data:
|
|
311
|
+
assessing geographic masking strategies for individual-level data.
|
|
312
|
+
*Advances in Medicine*, 2014, 567049. https://doi.org/10.1155/2014/567049
|
|
313
|
+
* Zhang, S., Freundschuh, S. M., Lenzer, K., & Zandbergen, P. A. (2017). The
|
|
314
|
+
location swapping method for geomasking. *Cartography and Geographic
|
|
315
|
+
Information Science*, 44(1), 22-34.
|
|
316
|
+
https://doi.org/10.1080/15230406.2015.1095655
|
|
317
|
+
|
|
318
|
+
## Citing
|
|
319
|
+
|
|
320
|
+
If you use `geomask` in research, please cite it using the metadata in
|
|
321
|
+
[`CITATION.cff`](CITATION.cff) and cite the original method papers above.
|
|
322
|
+
|
|
323
|
+
## License
|
|
324
|
+
|
|
325
|
+
MIT (c) Yusuf Eminoğlu. See [LICENSE](LICENSE).
|