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.
Files changed (37) hide show
  1. geomask-0.2.0/CHANGELOG.md +44 -0
  2. geomask-0.2.0/CITATION.cff +20 -0
  3. geomask-0.2.0/LICENSE +21 -0
  4. geomask-0.2.0/MANIFEST.in +9 -0
  5. geomask-0.2.0/PKG-INFO +325 -0
  6. geomask-0.2.0/README.md +285 -0
  7. geomask-0.2.0/examples/compare_methods.py +30 -0
  8. geomask-0.2.0/pyproject.toml +122 -0
  9. geomask-0.2.0/setup.cfg +4 -0
  10. geomask-0.2.0/src/geomask/__init__.py +103 -0
  11. geomask-0.2.0/src/geomask/_spatial.py +178 -0
  12. geomask-0.2.0/src/geomask/_utils.py +77 -0
  13. geomask-0.2.0/src/geomask/aae.py +214 -0
  14. geomask-0.2.0/src/geomask/aggregation.py +183 -0
  15. geomask-0.2.0/src/geomask/cli.py +236 -0
  16. geomask-0.2.0/src/geomask/evaluate.py +205 -0
  17. geomask-0.2.0/src/geomask/io.py +80 -0
  18. geomask-0.2.0/src/geomask/masking.py +336 -0
  19. geomask-0.2.0/src/geomask/privacy.py +127 -0
  20. geomask-0.2.0/src/geomask/projection.py +223 -0
  21. geomask-0.2.0/src/geomask/py.typed +0 -0
  22. geomask-0.2.0/src/geomask/synthetic.py +65 -0
  23. geomask-0.2.0/src/geomask/utility.py +274 -0
  24. geomask-0.2.0/src/geomask.egg-info/PKG-INFO +325 -0
  25. geomask-0.2.0/src/geomask.egg-info/SOURCES.txt +35 -0
  26. geomask-0.2.0/src/geomask.egg-info/dependency_links.txt +1 -0
  27. geomask-0.2.0/src/geomask.egg-info/entry_points.txt +2 -0
  28. geomask-0.2.0/src/geomask.egg-info/requires.txt +10 -0
  29. geomask-0.2.0/src/geomask.egg-info/top_level.txt +1 -0
  30. geomask-0.2.0/tests/conftest.py +38 -0
  31. geomask-0.2.0/tests/test_aggregation_aae.py +145 -0
  32. geomask-0.2.0/tests/test_cli_io.py +189 -0
  33. geomask-0.2.0/tests/test_evaluate.py +65 -0
  34. geomask-0.2.0/tests/test_masking.py +246 -0
  35. geomask-0.2.0/tests/test_metrics.py +164 -0
  36. geomask-0.2.0/tests/test_projection.py +96 -0
  37. 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.
@@ -0,0 +1,9 @@
1
+ include LICENSE
2
+ include README.md
3
+ include CHANGELOG.md
4
+ include CITATION.cff
5
+ include pyproject.toml
6
+ include src/geomask/py.typed
7
+ recursive-include src/geomask *.py
8
+ recursive-include tests *.py
9
+ recursive-include examples *.py
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
+ [![CI](https://github.com/YusufEminoglu/geomask_sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/YusufEminoglu/geomask_sdk/actions/workflows/ci.yml)
44
+ [![PyPI](https://img.shields.io/pypi/v/geomask.svg)](https://pypi.org/project/geomask/)
45
+ [![Python](https://img.shields.io/pypi/pyversions/geomask.svg)](https://pypi.org/project/geomask/)
46
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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).