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.
- arealinterp-0.1.0/CHANGELOG.md +24 -0
- arealinterp-0.1.0/CITATION.cff +20 -0
- arealinterp-0.1.0/LICENSE +21 -0
- arealinterp-0.1.0/MANIFEST.in +9 -0
- arealinterp-0.1.0/PKG-INFO +297 -0
- arealinterp-0.1.0/README.md +260 -0
- arealinterp-0.1.0/examples/quickstart.py +77 -0
- arealinterp-0.1.0/pyproject.toml +114 -0
- arealinterp-0.1.0/setup.cfg +4 -0
- arealinterp-0.1.0/src/arealinterp/__init__.py +106 -0
- arealinterp-0.1.0/src/arealinterp/__main__.py +7 -0
- arealinterp-0.1.0/src/arealinterp/_version.py +1 -0
- arealinterp-0.1.0/src/arealinterp/areal.py +183 -0
- arealinterp-0.1.0/src/arealinterp/cli.py +295 -0
- arealinterp-0.1.0/src/arealinterp/dasymetric.py +502 -0
- arealinterp-0.1.0/src/arealinterp/diagnostics.py +120 -0
- arealinterp-0.1.0/src/arealinterp/geometry.py +463 -0
- arealinterp-0.1.0/src/arealinterp/io.py +169 -0
- arealinterp-0.1.0/src/arealinterp/overlay.py +219 -0
- arealinterp-0.1.0/src/arealinterp/py.typed +0 -0
- arealinterp-0.1.0/src/arealinterp/pycno.py +206 -0
- arealinterp-0.1.0/src/arealinterp/raster.py +209 -0
- arealinterp-0.1.0/src/arealinterp.egg-info/PKG-INFO +297 -0
- arealinterp-0.1.0/src/arealinterp.egg-info/SOURCES.txt +32 -0
- arealinterp-0.1.0/src/arealinterp.egg-info/dependency_links.txt +1 -0
- arealinterp-0.1.0/src/arealinterp.egg-info/entry_points.txt +2 -0
- arealinterp-0.1.0/src/arealinterp.egg-info/requires.txt +10 -0
- arealinterp-0.1.0/src/arealinterp.egg-info/top_level.txt +1 -0
- arealinterp-0.1.0/tests/conftest.py +61 -0
- arealinterp-0.1.0/tests/test_areal.py +158 -0
- arealinterp-0.1.0/tests/test_dasymetric.py +183 -0
- arealinterp-0.1.0/tests/test_geometry.py +169 -0
- arealinterp-0.1.0/tests/test_io_cli.py +437 -0
- 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,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
|
+
[](https://pypi.org/project/arealinterp/)
|
|
41
|
+
[](https://pypi.org/project/arealinterp/)
|
|
42
|
+
[](https://github.com/YusufEminoglu/arealinterp_sdk/actions/workflows/ci.yml)
|
|
43
|
+
[](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
|
+
[](https://pypi.org/project/arealinterp/)
|
|
4
|
+
[](https://pypi.org/project/arealinterp/)
|
|
5
|
+
[](https://github.com/YusufEminoglu/arealinterp_sdk/actions/workflows/ci.yml)
|
|
6
|
+
[](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).
|