arealinterp 0.1.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 (34) hide show
  1. arealinterp-0.1.0/CHANGELOG.md +24 -0
  2. arealinterp-0.1.0/CITATION.cff +20 -0
  3. arealinterp-0.1.0/LICENSE +21 -0
  4. arealinterp-0.1.0/MANIFEST.in +9 -0
  5. arealinterp-0.1.0/PKG-INFO +297 -0
  6. arealinterp-0.1.0/README.md +260 -0
  7. arealinterp-0.1.0/examples/quickstart.py +77 -0
  8. arealinterp-0.1.0/pyproject.toml +114 -0
  9. arealinterp-0.1.0/setup.cfg +4 -0
  10. arealinterp-0.1.0/src/arealinterp/__init__.py +106 -0
  11. arealinterp-0.1.0/src/arealinterp/__main__.py +7 -0
  12. arealinterp-0.1.0/src/arealinterp/_version.py +1 -0
  13. arealinterp-0.1.0/src/arealinterp/areal.py +183 -0
  14. arealinterp-0.1.0/src/arealinterp/cli.py +295 -0
  15. arealinterp-0.1.0/src/arealinterp/dasymetric.py +502 -0
  16. arealinterp-0.1.0/src/arealinterp/diagnostics.py +120 -0
  17. arealinterp-0.1.0/src/arealinterp/geometry.py +463 -0
  18. arealinterp-0.1.0/src/arealinterp/io.py +169 -0
  19. arealinterp-0.1.0/src/arealinterp/overlay.py +219 -0
  20. arealinterp-0.1.0/src/arealinterp/py.typed +0 -0
  21. arealinterp-0.1.0/src/arealinterp/pycno.py +206 -0
  22. arealinterp-0.1.0/src/arealinterp/raster.py +209 -0
  23. arealinterp-0.1.0/src/arealinterp.egg-info/PKG-INFO +297 -0
  24. arealinterp-0.1.0/src/arealinterp.egg-info/SOURCES.txt +32 -0
  25. arealinterp-0.1.0/src/arealinterp.egg-info/dependency_links.txt +1 -0
  26. arealinterp-0.1.0/src/arealinterp.egg-info/entry_points.txt +2 -0
  27. arealinterp-0.1.0/src/arealinterp.egg-info/requires.txt +10 -0
  28. arealinterp-0.1.0/src/arealinterp.egg-info/top_level.txt +1 -0
  29. arealinterp-0.1.0/tests/conftest.py +61 -0
  30. arealinterp-0.1.0/tests/test_areal.py +158 -0
  31. arealinterp-0.1.0/tests/test_dasymetric.py +183 -0
  32. arealinterp-0.1.0/tests/test_geometry.py +169 -0
  33. arealinterp-0.1.0/tests/test_io_cli.py +437 -0
  34. arealinterp-0.1.0/tests/test_pycno.py +115 -0
@@ -0,0 +1,24 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [0.1.0] - 2026-11-09
8
+ ### Added
9
+ - Pure-Python + numpy polygon engine: oriented regions (holes, multipolygons), exact
10
+ intersection and intersection area by boundary classification, vertex snapping, GeoJSON
11
+ conversion, vectorised point-in-polygon.
12
+ - Optional shapely backend (`backend="shapely"` / `"auto"`).
13
+ - `overlay()` sparse source x target intersection table.
14
+ - Areal weighting for extensive and intensive variables (Goodchild & Lam 1980), with
15
+ `allocate_total` option.
16
+ - Target-density weighting (Schroeder 2007).
17
+ - Dasymetric mapping with land-cover rasters or polygons: binary, relative density
18
+ (multi-class, Mennis 2003) and fixed percentages (Eicher & Brewer 2001); percent-cover
19
+ density estimation with presets; dasymetric surface output for rasters; fallbacks reported.
20
+ - Smooth pycnophylactic interpolation (Tobler 1979) with per-zone volume preservation,
21
+ non-negativity and Neumann / zero edge conditions.
22
+ - Volume-preservation diagnostics (`VolumeReport`) on every extensive result.
23
+ - GeoJSON, CSV and ESRI ASCII grid I/O; `arealinterp` command-line tool
24
+ (`overlay`, `areal`, `tdw`, `dasymetric`, `pycno`).
@@ -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: "geospacephilo@gmail.com"
7
+ orcid: "https://orcid.org/0009-0005-6000-2934"
8
+ title: "arealinterp"
9
+ version: "0.1.0"
10
+ date-released: "2026-11-09"
11
+ license: "MIT"
12
+ repository-code: "https://github.com/YusufEminoglu/arealinterp_sdk"
13
+ url: "https://pypi.org/project/arealinterp/"
14
+ abstract: "Areal interpolation between incompatible zone systems: area-weighted interpolation on an exact polygon overlay, target-density weighting, binary, multi-class and fixed-percentage dasymetric mapping with land-cover rasters or polygons, and Tobler's smooth pycnophylactic interpolation, with volume-preservation diagnostics, GeoJSON/CSV I/O and a CLI."
15
+ keywords:
16
+ - areal interpolation
17
+ - dasymetric mapping
18
+ - pycnophylactic interpolation
19
+ - census
20
+ - gis
@@ -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/arealinterp/py.typed
7
+ recursive-include src/arealinterp *.py
8
+ recursive-include tests *.py
9
+ recursive-include examples *.py *.geojson *.asc
@@ -0,0 +1,297 @@
1
+ Metadata-Version: 2.4
2
+ Name: arealinterp
3
+ Version: 0.1.0
4
+ Summary: Areal interpolation between incompatible zone systems: exact-overlay area weighting, target-density weighting, binary/multi-class dasymetric mapping and Tobler's pycnophylactic interpolation, with volume-preservation diagnostics.
5
+ Author-email: Yusuf Eminoğlu <geospacephilo@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/YusufEminoglu/arealinterp_sdk
8
+ Project-URL: Repository, https://github.com/YusufEminoglu/arealinterp_sdk
9
+ Project-URL: Issues, https://github.com/YusufEminoglu/arealinterp_sdk/issues
10
+ Project-URL: Changelog, https://github.com/YusufEminoglu/arealinterp_sdk/blob/main/CHANGELOG.md
11
+ Keywords: areal-interpolation,dasymetric-mapping,pycnophylactic,area-weighting,polygon-overlay,census,zone-systems,change-of-support,population,geojson,gis
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: Environment :: Console
21
+ Classifier: Topic :: Scientific/Engineering :: GIS
22
+ Classifier: Intended Audience :: Science/Research
23
+ Classifier: Intended Audience :: Developers
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.9
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: numpy>=1.21
29
+ Provides-Extra: shapely
30
+ Requires-Dist: shapely>=2.0; extra == "shapely"
31
+ Provides-Extra: dev
32
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
33
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
34
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
35
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
36
+ Dynamic: license-file
37
+
38
+ # arealinterp
39
+
40
+ [![PyPI version](https://img.shields.io/pypi/v/arealinterp.svg)](https://pypi.org/project/arealinterp/)
41
+ [![Python versions](https://img.shields.io/pypi/pyversions/arealinterp.svg)](https://pypi.org/project/arealinterp/)
42
+ [![CI](https://github.com/YusufEminoglu/arealinterp_sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/YusufEminoglu/arealinterp_sdk/actions/workflows/ci.yml)
43
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/YusufEminoglu/arealinterp_sdk/blob/main/LICENSE)
44
+
45
+ **Areal interpolation between incompatible zone systems.**
46
+
47
+ Census tracts never line up with planning districts, school catchments or
48
+ grid cells. `arealinterp` moves counts and rates from one set of polygons to
49
+ another with the classic methods, and it tells you how much of the total survived:
50
+
51
+ - **Areal weighting** for extensive (counts) and intensive (rates) variables
52
+ (Goodchild & Lam 1980), on an **exact polygon overlay**. A pure-Python
53
+ engine handles concave polygons, holes, multipolygons and shared
54
+ boundaries. The optional `shapely` backend is faster.
55
+ - **Target-density weighting** with an ancillary count on the target zones (Schroeder 2007).
56
+ - **Dasymetric mapping** with a land-cover **raster** or land-cover **polygons**:
57
+ - binary (inhabited / uninhabited),
58
+ - multi-class relative density (for example three-class), with densities
59
+ estimated by percent-cover sampling (Mennis 2003; Mennis & Hultgren 2006),
60
+ - fixed percentages per class (Eicher & Brewer 2001).
61
+ - **Pycnophylactic interpolation** on a grid (Tobler 1979) in numpy, with
62
+ each zone's volume preserved after every iteration and no negative values.
63
+ - **Volume-preservation diagnostics** for every extensive variable: totals,
64
+ relative error and the worst source zone.
65
+ - **GeoJSON / CSV / ESRI ASCII grid** I/O, plus a command-line tool.
66
+
67
+ ```bash
68
+ pip install arealinterp # numpy only
69
+ pip install "arealinterp[shapely]" # + shapely overlay backend
70
+ ```
71
+
72
+ Requires Python 3.9 or newer.
73
+
74
+ > Need more? [`tobler`](https://github.com/pysal/tobler) (PySAL) is a fuller
75
+ > alternative built on geopandas. It adds regression-based and model-based
76
+ > interpolation, H3 / hexagon utilities and raster masking through rasterio.
77
+ > `arealinterp` is a smaller package for when you don't want the geopandas
78
+ > stack. It has a dependency-free overlay engine, explicit diagnostics and a CLI.
79
+
80
+ ---
81
+
82
+ ## Quick start (Python)
83
+
84
+ ```python
85
+ from arealinterp import area_weighted, read_geojson, write_geojson
86
+
87
+ tracts = read_geojson("tracts.geojson", id_field="GEOID") # projected CRS!
88
+ districts = read_geojson("districts.geojson", id_field="code")
89
+
90
+ res = area_weighted(tracts, districts, extensive=["pop", "households"], intensive="median_rent")
91
+ print(res.as_dict("pop"))
92
+ print(res.volume["pop"]) # pop: source=152340 target=152340 diff=+0 (+0.000e+00)
93
+ write_geojson("districts_pop.geojson", districts, res)
94
+ ```
95
+
96
+ Dasymetric and pycnophylactic variants share the same inputs and result type:
97
+
98
+ ```python
99
+ from arealinterp import (binary_dasymetric, dasymetric, estimate_class_densities,
100
+ pycnophylactic_interpolate, read_ascii_grid)
101
+
102
+ lc = read_ascii_grid("landcover.asc") # 0 water, 1 low-, 2 high-density
103
+
104
+ bd = binary_dasymetric(tracts, districts, lc, "pop", inhabited=[1, 2])
105
+
106
+ est = estimate_class_densities(tracts, lc, "pop", threshold=0.8, presets={0: 0})
107
+ dd = dasymetric(tracts, districts, lc, "pop", densities=est.densities)
108
+ dd.details["surfaces"]["pop"] # the dasymetric population surface (Raster)
109
+
110
+ eb = dasymetric(tracts, districts, lc, "pop", percentages={2: 60, 1: 35, 0: 5})
111
+
112
+ pp = pycnophylactic_interpolate(tracts, districts, "pop", cell_size=100)
113
+ pp.details["pycno"]["pop"].surface # smooth surface, volume-preserving
114
+ ```
115
+
116
+ `examples/quickstart.py` runs all four methods on a synthetic town.
117
+
118
+ ## Quick start (command line)
119
+
120
+ ```bash
121
+ # area weighting, GeoJSON out (the legacy "crs" member of the target file is copied)
122
+ arealinterp areal tracts.geojson districts.geojson --source-id GEOID --target-id code \
123
+ -e pop -e households -i median_rent -o districts_pop.geojson
124
+
125
+ # attributes in a separate CSV
126
+ arealinterp areal tracts.geojson districts.geojson --source-id GEOID \
127
+ --join census.csv -e pop -o out.csv
128
+
129
+ # target-density weighting
130
+ arealinterp tdw tracts_2000.geojson tracts_2020.geojson -e pop2000 --target-weight pop2020 -o out.csv
131
+
132
+ # dasymetric: binary / densities / percentages / estimated densities
133
+ arealinterp dasymetric tracts.geojson districts.geojson -e pop --raster lc.asc --inhabited 1,2
134
+ arealinterp dasymetric tracts.geojson districts.geojson -e pop --landcover lc.geojson \
135
+ --class-field code --density high=10,low=3,other=0
136
+ arealinterp dasymetric tracts.geojson districts.geojson -e pop --raster lc.asc \
137
+ --estimate --threshold 0.8 --preset 0=0 --surface pop_surface.asc
138
+
139
+ # pycnophylactic surface only, or summed over targets
140
+ arealinterp pycno tracts.geojson -e pop --cell-size 100 --surface pop.asc
141
+ arealinterp pycno tracts.geojson districts.geojson -e pop --cell-size 100 -o out.csv
142
+
143
+ # the raw intersection table
144
+ arealinterp overlay tracts.geojson districts.geojson -o overlay.csv
145
+ ```
146
+
147
+ Without `-o` the estimates are written as CSV on stdout. The method summary
148
+ and the volume report go to stderr.
149
+
150
+ ## API overview
151
+
152
+ | Function | Purpose |
153
+ |---|---|
154
+ | `overlay(sources, targets, backend=)` | Exact intersection areas (`Overlay`: sparse table, `.matrix()`, `.records()`). |
155
+ | `area_weighted(sources, targets, extensive, intensive, allocate_total=)` | Goodchild & Lam areal weighting. |
156
+ | `target_density_weighted(sources, targets, extensive, target_variable)` | Schroeder TDW. |
157
+ | `dasymetric(sources, targets, ancillary, extensive, densities= / percentages=)` | Density or fixed-percentage dasymetric mapping (raster or polygons). |
158
+ | `binary_dasymetric(..., inhabited=[...])` | Binary dasymetric mapping. |
159
+ | `estimate_class_densities(sources, ancillary, variable, threshold=, presets=)` | Percent-cover sampling of class densities. |
160
+ | `pycnophylactic(sources, variable, cell_size)` / `pycnophylactic_interpolate(...)` | Tobler surface / surface summed over targets. |
161
+ | `intersection`, `intersection_area`, `make_polygon`, `box`, `region_area` | Pure-Python polygon geometry. |
162
+ | `read_geojson`, `write_geojson`, `write_csv`, `join_csv`, `read_ascii_grid`, `write_ascii_grid` | I/O. |
163
+ | `volume_report`, `VolumeReport`, `InterpolationResult` | Diagnostics and results. |
164
+
165
+ Zones are `Zone(id, geometry, properties)`. Geometries are lists of oriented
166
+ rings: exteriors counter-clockwise, holes clockwise. `region_from_geojson`
167
+ converts any GeoJSON Polygon or MultiPolygon, whatever its ring orientation.
168
+ Every method returns an `InterpolationResult`, which holds `values`
169
+ (`{variable: numpy array}`), `volume` (`{variable: VolumeReport}`), `notes`
170
+ (fallbacks and warnings) and `details` (backend, fallback sources,
171
+ surfaces, class densities).
172
+
173
+ ## Methods and sources
174
+
175
+ Notation: `y_s` is the source value, `A_s` the source area, `A_t` the target
176
+ area, `A_st` the area of `s ∩ t`, and `A_sc` / `A_stc` the area of land-cover
177
+ class `c` inside `s` / `s ∩ t`.
178
+
179
+ **Areal weighting** (Goodchild & Lam 1980):
180
+
181
+ - extensive: `y_t = Σ_s y_s · A_st / A_s`
182
+ - intensive: `y_t = Σ_s y_s · A_st / Σ_s A_st`. This is the area-weighted
183
+ mean over the part of `t` that the sources cover, NaN where nothing covers it.
184
+
185
+ With `allocate_total=True` the extensive denominator becomes `Σ_t A_st`. Each
186
+ source's whole value then goes to the targets it touches, which helps when
187
+ coastlines were digitised differently.
188
+
189
+ **Target-density weighting** (Schroeder 2007): `w_st = A_st · z_t / A_t`, and
190
+ `y_t = Σ_s y_s · w_st / Σ_t' w_st'`. Here `z_t` is an ancillary count on the
191
+ targets. Sources that touch only `z = 0` targets fall back to areal weighting.
192
+
193
+ **Dasymetric mapping**:
194
+
195
+ - density weighting: `y_t = Σ_s y_s · Σ_c d_c A_stc / Σ_c d_c A_sc`. Binary
196
+ mapping is `d_c ∈ {0, 1}`. Mennis (2003) uses a three-class urban density
197
+ layer.
198
+ - fixed percentages (Eicher & Brewer 2001): `p'_sc = p_c / Σ_{c present in s} p_c`,
199
+ and `y_t = Σ_s y_s · Σ_c p'_sc · A_stc / A_sc`.
200
+
201
+ With raster ancillary data, each cell belongs to the zones that contain its
202
+ centre, and the dasymetric surface is returned. With polygon ancillary data,
203
+ the triple overlay `s ∩ t ∩ c` is computed exactly. Area not covered by any
204
+ ancillary polygon, and nodata cells, form the class `UNCOVERED`. Sources with
205
+ no weighted area fall back to areal weighting. Sources too small to contain a
206
+ cell centre are interpolated with exact areal weighting. Both cases are
207
+ reported.
208
+
209
+ **Density estimation** (Mennis 2003; Mennis & Hultgren 2006): for each class,
210
+ the source zones covered by that class to at least `threshold` are pooled,
211
+ and `d_c = Σ y_s / Σ A_s`. Mennis & Hultgren (2006) compared 70 %, 80 % and
212
+ 90 % thresholds; `threshold=1.0` is "contained" sampling. You can preset
213
+ classes with `presets=`, for example water = 0. Classes that no zone samples
214
+ get one shared *residual* density: the population left after the known
215
+ classes, divided by their area. This rule is a simplification (see
216
+ Limitations).
217
+
218
+ **Pycnophylactic interpolation** (Tobler 1979). Each cell starts at its
219
+ zone's value divided by its cell count. Each iteration then:
220
+
221
+ 1. relaxes every cell towards the mean of its four rook neighbours, a Jacobi
222
+ step towards the Laplace equation that minimises roughness;
223
+ 2. adds each zone's mean residual back;
224
+ 3. clips negative cells to zero;
225
+ 4. rescales each zone multiplicatively.
226
+
227
+ Every zone's volume therefore holds to rounding error after every
228
+ iteration, which the tests check. The edge condition is either `neumann`
229
+ (zero gradient) or `zero` (zero density outside the study area). Both are
230
+ discussed by Tobler.
231
+
232
+ **Polygon overlay engine**. The boundary of `A ∩ B` is made of three parts:
233
+ the parts of ∂A inside B, the parts of ∂B inside A, and the boundary pieces
234
+ the two share running in the *same* direction. Pieces shared in *opposite*
235
+ directions are common edges of adjacent zones and are dropped. The engine
236
+ splits each edge at every intersection point and classifies each piece by
237
+ its midpoint. The area then follows from the shoelace / Green's theorem sum
238
+ over the kept pieces, and the pieces are chained back into rings. Nearly
239
+ coincident vertices are snapped together within a relative tolerance
240
+ (default 1e‑9 of the extent). The tests compare the engine with shapely on
241
+ random concave polygons with holes, to 1e‑12.
242
+
243
+ ### References
244
+
245
+ - Eicher, C. L. & Brewer, C. A. (2001). Dasymetric mapping and areal interpolation: implementation and evaluation. *Cartography and Geographic Information Science* 28(2), 125–138.
246
+ - Goodchild, M. F. & Lam, N. S.-N. (1980). Areal interpolation: a variant of the traditional spatial problem. *Geo-Processing* 1, 297–312.
247
+ - Mennis, J. (2003). Generating surface models of population using dasymetric mapping. *The Professional Geographer* 55(1), 31–42.
248
+ - Mennis, J. & Hultgren, T. (2006). Intelligent dasymetric mapping and its application to areal interpolation. *Cartography and Geographic Information Science* 33(3), 179–194.
249
+ - Schroeder, J. P. (2007). Target-density weighting interpolation and uncertainty evaluation for temporal analysis of census data. *Geographical Analysis* 39(3), 311–335.
250
+ - Tobler, W. R. (1979). Smooth pycnophylactic interpolation for geographical regions. *Journal of the American Statistical Association* 74(367), 519–530.
251
+
252
+ ## Limitations
253
+
254
+ - **Planar coordinates only.** Areas are shoelace areas in map units, so
255
+ reproject longitude/latitude data to an equal-area or local projected CRS
256
+ first. The CLI warns when coordinates look geographic. There is no CRS
257
+ handling or reprojection.
258
+ - Input polygons must be valid: rings must not self-intersect, and the zones
259
+ within one layer must not overlap. Ancillary land-cover polygons must not
260
+ overlap each other.
261
+ - The pure-Python overlay is O(n·m) in the vertex counts of each candidate
262
+ pair. That is fine for thousands of ordinary census polygons, but use
263
+ `backend="shapely"` for large or very detailed layers.
264
+ - Raster dasymetric mapping and the pycnophylactic grid assign cells by their
265
+ centres, so precision depends on the cell size relative to the zones. Small
266
+ zones may contain no cell centre. The dasymetric method then falls back to
267
+ exact areal weighting. The pycnophylactic method loses their volume and
268
+ reports them.
269
+ - The residual density for unsampled classes in `estimate_class_densities`
270
+ is a pragmatic simplification. It is not a line-by-line reimplementation of
271
+ Mennis & Hultgren's (2006) intelligent dasymetric mapping. Preset classes
272
+ you know better, or give `densities=` directly.
273
+ - Fixed-percentage weights (Eicher & Brewer 2001) are analyst choices. The
274
+ package ships no default percentages.
275
+ - Pycnophylactic iteration uses a relaxed Jacobi scheme with a relative
276
+ stopping tolerance (default `1e-4`). The surface is smooth but is not
277
+ solved to machine precision.
278
+ - ESRI ASCII grid is the only raster format. Convert GeoTIFFs with GDAL
279
+ (`gdal_translate -of AAIGrid`) or pass a numpy array as `Raster(data, Grid(...))`.
280
+
281
+ ## Academic citation
282
+
283
+ ```bibtex
284
+ @software{eminoglu_arealinterp,
285
+ author = {Eminoğlu, Yusuf},
286
+ title = {arealinterp},
287
+ year = {2026},
288
+ version = {0.1.0},
289
+ url = {https://github.com/YusufEminoglu/arealinterp_sdk}
290
+ }
291
+ ```
292
+
293
+ See also `CITATION.cff`.
294
+
295
+ ## License
296
+
297
+ MIT © 2026 Yusuf Eminoğlu. See [LICENSE](LICENSE).
@@ -0,0 +1,260 @@
1
+ # arealinterp
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/arealinterp.svg)](https://pypi.org/project/arealinterp/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/arealinterp.svg)](https://pypi.org/project/arealinterp/)
5
+ [![CI](https://github.com/YusufEminoglu/arealinterp_sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/YusufEminoglu/arealinterp_sdk/actions/workflows/ci.yml)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/YusufEminoglu/arealinterp_sdk/blob/main/LICENSE)
7
+
8
+ **Areal interpolation between incompatible zone systems.**
9
+
10
+ Census tracts never line up with planning districts, school catchments or
11
+ grid cells. `arealinterp` moves counts and rates from one set of polygons to
12
+ another with the classic methods, and it tells you how much of the total survived:
13
+
14
+ - **Areal weighting** for extensive (counts) and intensive (rates) variables
15
+ (Goodchild & Lam 1980), on an **exact polygon overlay**. A pure-Python
16
+ engine handles concave polygons, holes, multipolygons and shared
17
+ boundaries. The optional `shapely` backend is faster.
18
+ - **Target-density weighting** with an ancillary count on the target zones (Schroeder 2007).
19
+ - **Dasymetric mapping** with a land-cover **raster** or land-cover **polygons**:
20
+ - binary (inhabited / uninhabited),
21
+ - multi-class relative density (for example three-class), with densities
22
+ estimated by percent-cover sampling (Mennis 2003; Mennis & Hultgren 2006),
23
+ - fixed percentages per class (Eicher & Brewer 2001).
24
+ - **Pycnophylactic interpolation** on a grid (Tobler 1979) in numpy, with
25
+ each zone's volume preserved after every iteration and no negative values.
26
+ - **Volume-preservation diagnostics** for every extensive variable: totals,
27
+ relative error and the worst source zone.
28
+ - **GeoJSON / CSV / ESRI ASCII grid** I/O, plus a command-line tool.
29
+
30
+ ```bash
31
+ pip install arealinterp # numpy only
32
+ pip install "arealinterp[shapely]" # + shapely overlay backend
33
+ ```
34
+
35
+ Requires Python 3.9 or newer.
36
+
37
+ > Need more? [`tobler`](https://github.com/pysal/tobler) (PySAL) is a fuller
38
+ > alternative built on geopandas. It adds regression-based and model-based
39
+ > interpolation, H3 / hexagon utilities and raster masking through rasterio.
40
+ > `arealinterp` is a smaller package for when you don't want the geopandas
41
+ > stack. It has a dependency-free overlay engine, explicit diagnostics and a CLI.
42
+
43
+ ---
44
+
45
+ ## Quick start (Python)
46
+
47
+ ```python
48
+ from arealinterp import area_weighted, read_geojson, write_geojson
49
+
50
+ tracts = read_geojson("tracts.geojson", id_field="GEOID") # projected CRS!
51
+ districts = read_geojson("districts.geojson", id_field="code")
52
+
53
+ res = area_weighted(tracts, districts, extensive=["pop", "households"], intensive="median_rent")
54
+ print(res.as_dict("pop"))
55
+ print(res.volume["pop"]) # pop: source=152340 target=152340 diff=+0 (+0.000e+00)
56
+ write_geojson("districts_pop.geojson", districts, res)
57
+ ```
58
+
59
+ Dasymetric and pycnophylactic variants share the same inputs and result type:
60
+
61
+ ```python
62
+ from arealinterp import (binary_dasymetric, dasymetric, estimate_class_densities,
63
+ pycnophylactic_interpolate, read_ascii_grid)
64
+
65
+ lc = read_ascii_grid("landcover.asc") # 0 water, 1 low-, 2 high-density
66
+
67
+ bd = binary_dasymetric(tracts, districts, lc, "pop", inhabited=[1, 2])
68
+
69
+ est = estimate_class_densities(tracts, lc, "pop", threshold=0.8, presets={0: 0})
70
+ dd = dasymetric(tracts, districts, lc, "pop", densities=est.densities)
71
+ dd.details["surfaces"]["pop"] # the dasymetric population surface (Raster)
72
+
73
+ eb = dasymetric(tracts, districts, lc, "pop", percentages={2: 60, 1: 35, 0: 5})
74
+
75
+ pp = pycnophylactic_interpolate(tracts, districts, "pop", cell_size=100)
76
+ pp.details["pycno"]["pop"].surface # smooth surface, volume-preserving
77
+ ```
78
+
79
+ `examples/quickstart.py` runs all four methods on a synthetic town.
80
+
81
+ ## Quick start (command line)
82
+
83
+ ```bash
84
+ # area weighting, GeoJSON out (the legacy "crs" member of the target file is copied)
85
+ arealinterp areal tracts.geojson districts.geojson --source-id GEOID --target-id code \
86
+ -e pop -e households -i median_rent -o districts_pop.geojson
87
+
88
+ # attributes in a separate CSV
89
+ arealinterp areal tracts.geojson districts.geojson --source-id GEOID \
90
+ --join census.csv -e pop -o out.csv
91
+
92
+ # target-density weighting
93
+ arealinterp tdw tracts_2000.geojson tracts_2020.geojson -e pop2000 --target-weight pop2020 -o out.csv
94
+
95
+ # dasymetric: binary / densities / percentages / estimated densities
96
+ arealinterp dasymetric tracts.geojson districts.geojson -e pop --raster lc.asc --inhabited 1,2
97
+ arealinterp dasymetric tracts.geojson districts.geojson -e pop --landcover lc.geojson \
98
+ --class-field code --density high=10,low=3,other=0
99
+ arealinterp dasymetric tracts.geojson districts.geojson -e pop --raster lc.asc \
100
+ --estimate --threshold 0.8 --preset 0=0 --surface pop_surface.asc
101
+
102
+ # pycnophylactic surface only, or summed over targets
103
+ arealinterp pycno tracts.geojson -e pop --cell-size 100 --surface pop.asc
104
+ arealinterp pycno tracts.geojson districts.geojson -e pop --cell-size 100 -o out.csv
105
+
106
+ # the raw intersection table
107
+ arealinterp overlay tracts.geojson districts.geojson -o overlay.csv
108
+ ```
109
+
110
+ Without `-o` the estimates are written as CSV on stdout. The method summary
111
+ and the volume report go to stderr.
112
+
113
+ ## API overview
114
+
115
+ | Function | Purpose |
116
+ |---|---|
117
+ | `overlay(sources, targets, backend=)` | Exact intersection areas (`Overlay`: sparse table, `.matrix()`, `.records()`). |
118
+ | `area_weighted(sources, targets, extensive, intensive, allocate_total=)` | Goodchild & Lam areal weighting. |
119
+ | `target_density_weighted(sources, targets, extensive, target_variable)` | Schroeder TDW. |
120
+ | `dasymetric(sources, targets, ancillary, extensive, densities= / percentages=)` | Density or fixed-percentage dasymetric mapping (raster or polygons). |
121
+ | `binary_dasymetric(..., inhabited=[...])` | Binary dasymetric mapping. |
122
+ | `estimate_class_densities(sources, ancillary, variable, threshold=, presets=)` | Percent-cover sampling of class densities. |
123
+ | `pycnophylactic(sources, variable, cell_size)` / `pycnophylactic_interpolate(...)` | Tobler surface / surface summed over targets. |
124
+ | `intersection`, `intersection_area`, `make_polygon`, `box`, `region_area` | Pure-Python polygon geometry. |
125
+ | `read_geojson`, `write_geojson`, `write_csv`, `join_csv`, `read_ascii_grid`, `write_ascii_grid` | I/O. |
126
+ | `volume_report`, `VolumeReport`, `InterpolationResult` | Diagnostics and results. |
127
+
128
+ Zones are `Zone(id, geometry, properties)`. Geometries are lists of oriented
129
+ rings: exteriors counter-clockwise, holes clockwise. `region_from_geojson`
130
+ converts any GeoJSON Polygon or MultiPolygon, whatever its ring orientation.
131
+ Every method returns an `InterpolationResult`, which holds `values`
132
+ (`{variable: numpy array}`), `volume` (`{variable: VolumeReport}`), `notes`
133
+ (fallbacks and warnings) and `details` (backend, fallback sources,
134
+ surfaces, class densities).
135
+
136
+ ## Methods and sources
137
+
138
+ Notation: `y_s` is the source value, `A_s` the source area, `A_t` the target
139
+ area, `A_st` the area of `s ∩ t`, and `A_sc` / `A_stc` the area of land-cover
140
+ class `c` inside `s` / `s ∩ t`.
141
+
142
+ **Areal weighting** (Goodchild & Lam 1980):
143
+
144
+ - extensive: `y_t = Σ_s y_s · A_st / A_s`
145
+ - intensive: `y_t = Σ_s y_s · A_st / Σ_s A_st`. This is the area-weighted
146
+ mean over the part of `t` that the sources cover, NaN where nothing covers it.
147
+
148
+ With `allocate_total=True` the extensive denominator becomes `Σ_t A_st`. Each
149
+ source's whole value then goes to the targets it touches, which helps when
150
+ coastlines were digitised differently.
151
+
152
+ **Target-density weighting** (Schroeder 2007): `w_st = A_st · z_t / A_t`, and
153
+ `y_t = Σ_s y_s · w_st / Σ_t' w_st'`. Here `z_t` is an ancillary count on the
154
+ targets. Sources that touch only `z = 0` targets fall back to areal weighting.
155
+
156
+ **Dasymetric mapping**:
157
+
158
+ - density weighting: `y_t = Σ_s y_s · Σ_c d_c A_stc / Σ_c d_c A_sc`. Binary
159
+ mapping is `d_c ∈ {0, 1}`. Mennis (2003) uses a three-class urban density
160
+ layer.
161
+ - fixed percentages (Eicher & Brewer 2001): `p'_sc = p_c / Σ_{c present in s} p_c`,
162
+ and `y_t = Σ_s y_s · Σ_c p'_sc · A_stc / A_sc`.
163
+
164
+ With raster ancillary data, each cell belongs to the zones that contain its
165
+ centre, and the dasymetric surface is returned. With polygon ancillary data,
166
+ the triple overlay `s ∩ t ∩ c` is computed exactly. Area not covered by any
167
+ ancillary polygon, and nodata cells, form the class `UNCOVERED`. Sources with
168
+ no weighted area fall back to areal weighting. Sources too small to contain a
169
+ cell centre are interpolated with exact areal weighting. Both cases are
170
+ reported.
171
+
172
+ **Density estimation** (Mennis 2003; Mennis & Hultgren 2006): for each class,
173
+ the source zones covered by that class to at least `threshold` are pooled,
174
+ and `d_c = Σ y_s / Σ A_s`. Mennis & Hultgren (2006) compared 70 %, 80 % and
175
+ 90 % thresholds; `threshold=1.0` is "contained" sampling. You can preset
176
+ classes with `presets=`, for example water = 0. Classes that no zone samples
177
+ get one shared *residual* density: the population left after the known
178
+ classes, divided by their area. This rule is a simplification (see
179
+ Limitations).
180
+
181
+ **Pycnophylactic interpolation** (Tobler 1979). Each cell starts at its
182
+ zone's value divided by its cell count. Each iteration then:
183
+
184
+ 1. relaxes every cell towards the mean of its four rook neighbours, a Jacobi
185
+ step towards the Laplace equation that minimises roughness;
186
+ 2. adds each zone's mean residual back;
187
+ 3. clips negative cells to zero;
188
+ 4. rescales each zone multiplicatively.
189
+
190
+ Every zone's volume therefore holds to rounding error after every
191
+ iteration, which the tests check. The edge condition is either `neumann`
192
+ (zero gradient) or `zero` (zero density outside the study area). Both are
193
+ discussed by Tobler.
194
+
195
+ **Polygon overlay engine**. The boundary of `A ∩ B` is made of three parts:
196
+ the parts of ∂A inside B, the parts of ∂B inside A, and the boundary pieces
197
+ the two share running in the *same* direction. Pieces shared in *opposite*
198
+ directions are common edges of adjacent zones and are dropped. The engine
199
+ splits each edge at every intersection point and classifies each piece by
200
+ its midpoint. The area then follows from the shoelace / Green's theorem sum
201
+ over the kept pieces, and the pieces are chained back into rings. Nearly
202
+ coincident vertices are snapped together within a relative tolerance
203
+ (default 1e‑9 of the extent). The tests compare the engine with shapely on
204
+ random concave polygons with holes, to 1e‑12.
205
+
206
+ ### References
207
+
208
+ - Eicher, C. L. & Brewer, C. A. (2001). Dasymetric mapping and areal interpolation: implementation and evaluation. *Cartography and Geographic Information Science* 28(2), 125–138.
209
+ - Goodchild, M. F. & Lam, N. S.-N. (1980). Areal interpolation: a variant of the traditional spatial problem. *Geo-Processing* 1, 297–312.
210
+ - Mennis, J. (2003). Generating surface models of population using dasymetric mapping. *The Professional Geographer* 55(1), 31–42.
211
+ - Mennis, J. & Hultgren, T. (2006). Intelligent dasymetric mapping and its application to areal interpolation. *Cartography and Geographic Information Science* 33(3), 179–194.
212
+ - Schroeder, J. P. (2007). Target-density weighting interpolation and uncertainty evaluation for temporal analysis of census data. *Geographical Analysis* 39(3), 311–335.
213
+ - Tobler, W. R. (1979). Smooth pycnophylactic interpolation for geographical regions. *Journal of the American Statistical Association* 74(367), 519–530.
214
+
215
+ ## Limitations
216
+
217
+ - **Planar coordinates only.** Areas are shoelace areas in map units, so
218
+ reproject longitude/latitude data to an equal-area or local projected CRS
219
+ first. The CLI warns when coordinates look geographic. There is no CRS
220
+ handling or reprojection.
221
+ - Input polygons must be valid: rings must not self-intersect, and the zones
222
+ within one layer must not overlap. Ancillary land-cover polygons must not
223
+ overlap each other.
224
+ - The pure-Python overlay is O(n·m) in the vertex counts of each candidate
225
+ pair. That is fine for thousands of ordinary census polygons, but use
226
+ `backend="shapely"` for large or very detailed layers.
227
+ - Raster dasymetric mapping and the pycnophylactic grid assign cells by their
228
+ centres, so precision depends on the cell size relative to the zones. Small
229
+ zones may contain no cell centre. The dasymetric method then falls back to
230
+ exact areal weighting. The pycnophylactic method loses their volume and
231
+ reports them.
232
+ - The residual density for unsampled classes in `estimate_class_densities`
233
+ is a pragmatic simplification. It is not a line-by-line reimplementation of
234
+ Mennis & Hultgren's (2006) intelligent dasymetric mapping. Preset classes
235
+ you know better, or give `densities=` directly.
236
+ - Fixed-percentage weights (Eicher & Brewer 2001) are analyst choices. The
237
+ package ships no default percentages.
238
+ - Pycnophylactic iteration uses a relaxed Jacobi scheme with a relative
239
+ stopping tolerance (default `1e-4`). The surface is smooth but is not
240
+ solved to machine precision.
241
+ - ESRI ASCII grid is the only raster format. Convert GeoTIFFs with GDAL
242
+ (`gdal_translate -of AAIGrid`) or pass a numpy array as `Raster(data, Grid(...))`.
243
+
244
+ ## Academic citation
245
+
246
+ ```bibtex
247
+ @software{eminoglu_arealinterp,
248
+ author = {Eminoğlu, Yusuf},
249
+ title = {arealinterp},
250
+ year = {2026},
251
+ version = {0.1.0},
252
+ url = {https://github.com/YusufEminoglu/arealinterp_sdk}
253
+ }
254
+ ```
255
+
256
+ See also `CITATION.cff`.
257
+
258
+ ## License
259
+
260
+ MIT © 2026 Yusuf Eminoğlu. See [LICENSE](LICENSE).