spatialsample 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 (38) hide show
  1. spatialsample-0.1.0/CHANGELOG.md +36 -0
  2. spatialsample-0.1.0/CITATION.cff +21 -0
  3. spatialsample-0.1.0/LICENSE +21 -0
  4. spatialsample-0.1.0/MANIFEST.in +8 -0
  5. spatialsample-0.1.0/PKG-INFO +309 -0
  6. spatialsample-0.1.0/README.md +262 -0
  7. spatialsample-0.1.0/pyproject.toml +127 -0
  8. spatialsample-0.1.0/setup.cfg +4 -0
  9. spatialsample-0.1.0/src/spatialsample/__init__.py +89 -0
  10. spatialsample-0.1.0/src/spatialsample/_frame.py +91 -0
  11. spatialsample-0.1.0/src/spatialsample/_types.py +36 -0
  12. spatialsample-0.1.0/src/spatialsample/accuracy.py +232 -0
  13. spatialsample-0.1.0/src/spatialsample/balance.py +144 -0
  14. spatialsample-0.1.0/src/spatialsample/bas.py +96 -0
  15. spatialsample-0.1.0/src/spatialsample/cli.py +184 -0
  16. spatialsample-0.1.0/src/spatialsample/designs.py +562 -0
  17. spatialsample-0.1.0/src/spatialsample/estimators.py +222 -0
  18. spatialsample-0.1.0/src/spatialsample/geometry.py +253 -0
  19. spatialsample-0.1.0/src/spatialsample/grts.py +175 -0
  20. spatialsample-0.1.0/src/spatialsample/io.py +161 -0
  21. spatialsample-0.1.0/src/spatialsample/lpm.py +133 -0
  22. spatialsample-0.1.0/src/spatialsample/probabilities.py +202 -0
  23. spatialsample-0.1.0/src/spatialsample/py.typed +0 -0
  24. spatialsample-0.1.0/src/spatialsample/raster.py +170 -0
  25. spatialsample-0.1.0/src/spatialsample/sample.py +213 -0
  26. spatialsample-0.1.0/src/spatialsample.egg-info/PKG-INFO +309 -0
  27. spatialsample-0.1.0/src/spatialsample.egg-info/SOURCES.txt +36 -0
  28. spatialsample-0.1.0/src/spatialsample.egg-info/dependency_links.txt +1 -0
  29. spatialsample-0.1.0/src/spatialsample.egg-info/entry_points.txt +2 -0
  30. spatialsample-0.1.0/src/spatialsample.egg-info/requires.txt +22 -0
  31. spatialsample-0.1.0/src/spatialsample.egg-info/top_level.txt +1 -0
  32. spatialsample-0.1.0/tests/conftest.py +46 -0
  33. spatialsample-0.1.0/tests/test_accuracy.py +78 -0
  34. spatialsample-0.1.0/tests/test_designs.py +312 -0
  35. spatialsample-0.1.0/tests/test_estimators_balance.py +127 -0
  36. spatialsample-0.1.0/tests/test_geometry.py +112 -0
  37. spatialsample-0.1.0/tests/test_io_cli.py +274 -0
  38. spatialsample-0.1.0/tests/test_statistics.py +123 -0
@@ -0,0 +1,36 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project
5
+ uses [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0] - 2026-11-08
10
+ ### Added
11
+ - Sampling domains: `Region` (polygons and multipolygons with holes, built from GeoJSON,
12
+ Shapely/GeoPandas `__geo_interface__` or coordinates) and `RasterMask` (classified rasters
13
+ with a north-up geotransform). Pure NumPy point-in-polygon tests.
14
+ - Designs: simple random (`srs`), systematic square/hexagonal grid with random start and
15
+ optional random rotation (`systematic`), stratified by polygons or raster classes with
16
+ proportional, equal or Neyman allocation (`stratified`), stratified pixel sampling for map
17
+ accuracy assessment (`stratified_pixels`), GRTS with hierarchical address randomisation,
18
+ reverse hierarchical ordering and oversample sites (`grts`), Balanced Acceptance Sampling
19
+ with a random-start Halton sequence (`bas`), the local pivotal method LPM2 with unequal
20
+ inclusion probabilities (`lpm`), Poisson-disk / minimum-distance sampling (`poisson_disk`),
21
+ cluster plots (`cluster`), two-stage PPS sampling (`two_stage`), systematic points along
22
+ lines (`points_along_lines`) and random transects (`random_transects`).
23
+ - Every design returns a `Sample` with inclusion probabilities (or densities), design
24
+ weights, site type (base/over) and visiting order, exportable to GeoJSON, CSV and
25
+ GeoPandas.
26
+ - Estimators: Horvitz-Thompson total and mean, Hajek ratio mean, SRS (with-replacement)
27
+ variance and the local neighbourhood variance estimator of Stevens & Olsen (2003).
28
+ - Voronoi spatial-balance metric (Stevens & Olsen 2004) and `compare_designs`.
29
+ - Map accuracy assessment after Olofsson et al. (2014): area, user's, producer's and overall
30
+ accuracy with standard errors and confidence intervals, sample-size formula and
31
+ allocation helper. Reproduces the published worked example.
32
+ - Command-line interface `spatialsample` with `srs`, `systematic`, `grts`, `bas`, `lpm`,
33
+ `poisson`, `stratified`, `balance`, `compare`, `olofsson` and `samplesize` commands.
34
+
35
+ [Unreleased]: https://github.com/YusufEminoglu/spatialsample_sdk/compare/v0.1.0...HEAD
36
+ [0.1.0]: https://github.com/YusufEminoglu/spatialsample_sdk/releases/tag/v0.1.0
@@ -0,0 +1,21 @@
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: "spatialsample"
9
+ version: "0.1.0"
10
+ date-released: "2026-11-08"
11
+ license: "MIT"
12
+ repository-code: "https://github.com/YusufEminoglu/spatialsample_sdk"
13
+ url: "https://pypi.org/project/spatialsample/"
14
+ abstract: "Design-based spatial sampling in Python: GRTS, Balanced Acceptance Sampling, the local pivotal method, stratified, systematic, cluster, two-stage and transect designs, Horvitz-Thompson estimation with local-neighbourhood variance, the Voronoi spatial-balance metric and Olofsson et al. (2014) map accuracy assessment."
15
+ keywords:
16
+ - spatial sampling
17
+ - survey design
18
+ - GRTS
19
+ - balanced acceptance sampling
20
+ - local pivotal method
21
+ - accuracy assessment
@@ -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,8 @@
1
+ include LICENSE
2
+ include README.md
3
+ include CHANGELOG.md
4
+ include CITATION.cff
5
+ include pyproject.toml
6
+ include src/spatialsample/py.typed
7
+ recursive-include src/spatialsample *.py
8
+ recursive-include tests *.py
@@ -0,0 +1,309 @@
1
+ Metadata-Version: 2.4
2
+ Name: spatialsample
3
+ Version: 0.1.0
4
+ Summary: Design-based spatial sampling in Python: GRTS, BAS, LPM, stratified and systematic designs, Horvitz-Thompson estimation, spatial balance and Olofsson map accuracy assessment.
5
+ Author-email: Yusuf Eminoğlu <yusufeminoglu@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/YusufEminoglu/spatialsample_sdk
8
+ Project-URL: Repository, https://github.com/YusufEminoglu/spatialsample_sdk
9
+ Project-URL: Issues, https://github.com/YusufEminoglu/spatialsample_sdk/issues
10
+ Project-URL: Changelog, https://github.com/YusufEminoglu/spatialsample_sdk/blob/main/CHANGELOG.md
11
+ Keywords: spatial-sampling,survey-design,grts,balanced-acceptance-sampling,local-pivotal-method,spatially-balanced,horvitz-thompson,stratified-sampling,accuracy-assessment,land-cover,ecology,field-survey,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: Topic :: Scientific/Engineering :: GIS
21
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
22
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
23
+ Classifier: Intended Audience :: Science/Research
24
+ Classifier: Intended Audience :: Developers
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.9
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ Requires-Dist: numpy>=1.20.0
30
+ Provides-Extra: scipy
31
+ Requires-Dist: scipy>=1.7.0; extra == "scipy"
32
+ Provides-Extra: shapely
33
+ Requires-Dist: shapely>=2.0.0; extra == "shapely"
34
+ Provides-Extra: geo
35
+ Requires-Dist: shapely>=2.0.0; extra == "geo"
36
+ Requires-Dist: geopandas>=0.12.0; extra == "geo"
37
+ Provides-Extra: all
38
+ Requires-Dist: scipy>=1.7.0; extra == "all"
39
+ Requires-Dist: shapely>=2.0.0; extra == "all"
40
+ Requires-Dist: geopandas>=0.12.0; extra == "all"
41
+ Provides-Extra: dev
42
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
43
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
44
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
45
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
46
+ Dynamic: license-file
47
+
48
+ # spatialsample
49
+
50
+ [![PyPI version](https://img.shields.io/pypi/v/spatialsample.svg?color=10b981)](https://pypi.org/project/spatialsample/)
51
+ [![Python version support](https://img.shields.io/pypi/pyversions/spatialsample.svg?color=3b82f6)](https://pypi.org/project/spatialsample/)
52
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/YusufEminoglu/spatialsample_sdk/blob/main/LICENSE)
53
+ [![Code style: Ruff](https://img.shields.io/badge/code%20style-ruff-D7FF64.svg)](https://docs.astral.sh/ruff/)
54
+
55
+ **Design-based spatial sampling in Python.** Decide where to put field plots, household
56
+ survey points or validation samples for a land-cover map, with spatially balanced designs
57
+ (GRTS, BAS, LPM), classical designs, exact inclusion probabilities and design weights,
58
+ Horvitz-Thompson estimation, a spatial-balance metric to compare designs, and the
59
+ Olofsson et al. (2014) good-practice workflow for map accuracy and area estimation.
60
+
61
+ The core needs only NumPy. SciPy (faster nearest-neighbour search), Shapely and GeoPandas
62
+ are optional.
63
+
64
+ ## Installation
65
+
66
+ ```bash
67
+ pip install spatialsample # NumPy only
68
+ pip install "spatialsample[scipy]" # + SciPy KD-tree for large balance/frame computations
69
+ pip install "spatialsample[geo]" # + Shapely and GeoPandas interoperability
70
+ pip install "spatialsample[all]" # everything
71
+ ```
72
+
73
+ ## Quick start
74
+
75
+ ```python
76
+ import spatialsample as ss
77
+
78
+ # Study area in a projected CRS (metres): an L-shaped block with a lake (hole).
79
+ area = ss.Region.from_coords(
80
+ [(0, 0), (1000, 0), (1000, 600), (600, 600), (600, 1000), (0, 1000)],
81
+ holes=[[(200, 200), (400, 200), (400, 400), (200, 400)]],
82
+ )
83
+
84
+ sites = ss.grts(area, n=50, n_over=10, seed=1) # 50 base + 10 replacement sites
85
+ sites.to_geojson("sites.geojson") # or .to_csv(), .to_geodataframe()
86
+
87
+ print(sites.base().weight.sum()) # 800000.0 (= area; weights in m^2)
88
+ print(ss.spatial_balance(sites, domain=area).value) # Voronoi balance, lower is better
89
+
90
+ # After the field work: estimate the mean of a variable y observed at the base sites
91
+ base = sites.base()
92
+ y = observe(base.xy) # your measurements
93
+ est = ss.estimate(base, y) # HT total, ratio mean, local variance
94
+ print(est.mean, est.mean_se)
95
+ ```
96
+
97
+ Inputs can be a `Region`, a `RasterMask`, any GeoJSON mapping (`Polygon`, `MultiPolygon`,
98
+ `Feature`, `FeatureCollection`) or any object with `__geo_interface__` (Shapely, GeoPandas).
99
+ GRTS and LPM also accept an `(N, 2)` array of candidate sites (a finite frame, such as
100
+ households, lakes or pixels), optionally with a size measure `prob=` for
101
+ unequal-probability sampling.
102
+
103
+ > Coordinates are treated as planar. Reproject longitude/latitude data to a projected or
104
+ > equal-area CRS before sampling; distance-based designs and the balance metric assume
105
+ > linear units.
106
+
107
+ ## Designs
108
+
109
+ | Function | Design | Inclusion probabilities | Reference |
110
+ |---|---|---|---|
111
+ | `srs` | Simple random sampling of locations | exact, `n / area` | |
112
+ | `systematic` | Square or hexagonal grid, uniformly random start, optional random rotation | exact, `1 / cell area` (random `n`) | |
113
+ | `stratified` | Stratified by polygons or raster classes; proportional, equal or Neyman allocation; SRS, BAS, GRTS or LPM within strata | exact, `n_h / area_h` | Cochran (1977) |
114
+ | `stratified_pixels` | Stratified random sampling of raster pixels without replacement | exact, `n_h / N_h` | Olofsson et al. (2014) |
115
+ | `grts` | Generalized Random Tessellation Stratified: randomised quadrant-recursive addresses, systematic selection on the resulting line, reverse hierarchical ordering, oversample sites | exact (finite frames), equal or unequal | Stevens & Olsen (2004) |
116
+ | `bas` | Balanced Acceptance Sampling from a random-start Halton sequence (bases 2, 3); oversample continues the sequence | exact, `n / area` | Robertson et al. (2013) |
117
+ | `lpm` | Local pivotal method (LPM2), equal or unequal probabilities | exact | Grafström et al. (2012) |
118
+ | `poisson_disk` | Minimum-distance (simple sequential inhibition) sampling | approximate, `n / area` | |
119
+ | `cluster` | Cluster plots (FIA-style centre + 3 at 120 degrees, 2 x 2, line, cross or custom) around SRS/BAS/GRTS centres | `n_clusters / area` per subplot | |
120
+ | `two_stage` | PPS-by-area (or equal) selection of primary units, then SRS inside each | exact; self-weighting with PPS | |
121
+ | `points_along_lines` | Systematic points along roads/rivers/hedgerows with random start | exact, `1 / spacing` per unit length | |
122
+ | `random_transects` | Straight transects of fixed length and random (or fixed) bearing | `n / area` | |
123
+
124
+ Every design returns a `Sample` with `xy`, `pi` (inclusion probability for finite frames,
125
+ inclusion density per unit area for continuous designs, following Cordy 1993), `weight =
126
+ 1/pi`, `site_type` (`base` / `over`), visiting `order`, optional `stratum`, `index` and
127
+ extra columns (`cluster`, `psu`, `chainage`, pixel `row`/`col`, ...).
128
+
129
+ GRTS and LPM on a continuous area use a dense, randomly offset grid frame (default
130
+ `max(4000, 100 (n + n_over))` cells for GRTS) and move each selected cell centre to a
131
+ uniform location within its cell, as recent versions of the R package spsurvey do.
132
+
133
+ ### Which design is most spread out?
134
+
135
+ Voronoi spatial balance (Stevens & Olsen 2004): the variance of the summed inclusion
136
+ probability inside each site's Voronoi polygon, `mean((v_i - 1)^2)`. Zero is perfectly
137
+ balanced. Measured with `ss.compare_designs` on the L-shaped area above (800,000 m^2, with
138
+ a hole), `n = 50`, 100 independent draws per design, 20,000-point evaluation grid:
139
+
140
+ | Design | Voronoi balance, mean | sd |
141
+ |---|---:|---:|
142
+ | systematic, square (random rotation) | 0.041 | 0.011 |
143
+ | systematic, hexagonal (random rotation) | 0.044 | 0.008 |
144
+ | LPM | 0.074 | 0.017 |
145
+ | BAS | 0.087 | 0.021 |
146
+ | GRTS | 0.105 | 0.024 |
147
+ | Poisson-disk (default distance) | 0.118 | 0.033 |
148
+ | simple random | 0.352 | 0.106 |
149
+
150
+ Systematic grids are the most even but have a random sample size, no valid design-based
151
+ variance estimator and are vulnerable to periodic patterns; GRTS, BAS and LPM give fixed
152
+ sample sizes, exact inclusion probabilities and three to five times better balance than
153
+ simple random sampling. Reproduce with `spatialsample compare area.geojson --n 50 --reps 100`.
154
+
155
+ ### Does balance pay off?
156
+
157
+ Estimating the mean of the smooth surface `10 + 0.01 x + 5 sin(y / 150)` over the same area
158
+ with `n = 30` (400 draws per design, ratio estimator, `ss.estimate` defaults):
159
+
160
+ | Design | bias | true SD of the estimate | mean estimated SE |
161
+ |---|---:|---:|---:|
162
+ | simple random | +0.026 | 0.905 | 0.880 (SRS variance) |
163
+ | GRTS | -0.015 | 0.331 | 0.399 (local variance) |
164
+ | LPM | -0.026 | 0.282 | 0.423 (local variance) |
165
+ | BAS | +0.011 | 0.180 | 0.426 (local variance) |
166
+ | systematic, hexagonal | -0.003 | 0.185 | 0.456 (local variance) |
167
+
168
+ All biases are within Monte Carlo error. Spatially balanced designs cut the standard error
169
+ by a factor of 2.7 (GRTS) to 5 (BAS) here; the local neighbourhood variance estimator captures much of that
170
+ gain while staying on the conservative side for smooth surfaces.
171
+
172
+ ## Estimation
173
+
174
+ ```python
175
+ ss.ht_total(y, pi) # Horvitz-Thompson total (integral for continuous designs)
176
+ ss.ht_mean(y, pi, size=area) # HT mean; without size: Hajek ratio mean
177
+ ss.srs_variance(y, pi) # with-replacement variance of the HT total
178
+ ss.local_variance(y, xy, pi, k=4) # Stevens & Olsen (2003) neighbourhood variance
179
+ ss.estimate(sample, y) # all of the above with a CI, design-aware defaults
180
+ ```
181
+
182
+ `local_variance` uses neighbourhoods of each site and its three nearest sample neighbours,
183
+ initial weights proportional to `1/pi` balanced to a doubly stochastic matrix by
184
+ iterative proportional fitting. By default each neighbourhood term is divided by
185
+ `1 - sum_j w_ij^2`, the usual small-sample correction for a weighted variance, which makes
186
+ the estimator unbiased (relative to the with-replacement variance) when the variable has no
187
+ spatial structure; pass `correction=False` for the uncorrected form, which is about 25 %
188
+ smaller with `k = 4`. Tests check both properties by simulation.
189
+
190
+ ## Map accuracy and area estimation (Olofsson et al. 2014)
191
+
192
+ ```python
193
+ import spatialsample as ss
194
+
195
+ # Section 5 worked example of Olofsson et al. (2014): sample counts, rows = map class,
196
+ # columns = reference class; mapped areas in 30 m pixels (0.09 ha each).
197
+ matrix = [[66, 0, 5, 4],
198
+ [0, 55, 8, 12],
199
+ [1, 0, 153, 11],
200
+ [2, 1, 9, 313]]
201
+ pixels = [200_000, 150_000, 3_200_000, 6_450_000]
202
+ labels = ["Deforestation", "Forest gain", "Stable forest", "Stable non-forest"]
203
+
204
+ res = ss.olofsson(matrix, [p * 0.09 for p in pixels], labels=labels)
205
+ print(res.summary())
206
+ ```
207
+
208
+ ```
209
+ class mapped area est. area +/- 95% CI user +/- producer +/-
210
+ -------------------------------------------------------------------------------------------------
211
+ Deforestation 18,000 21,158 6,158 0.880 0.074 0.749 0.213
212
+ Forest gain 13,500 11,686 3,756 0.733 0.101 0.847 0.254
213
+ Stable forest 288,000 285,770 15,510 0.927 0.040 0.935 0.034
214
+ Stable non-forest 580,500 581,386 16,281 0.963 0.021 0.962 0.018
215
+ overall accuracy 0.947 +/- 0.018 (95% CI), n = 640
216
+ ```
217
+
218
+ These reproduce the published estimates (for example a deforestation area of
219
+ 21,158 ha +/- 6,158 ha against 18,000 ha mapped; user's accuracy 0.88, producer's accuracy
220
+ 0.75 +/- 0.21; overall accuracy 0.95). The paper's sample-size example (conjectured user's
221
+ accuracies 0.7, 0.6, 0.9, 0.95 and a target standard error of 0.01 for overall accuracy) is
222
+ `ss.sample_size(pixels, [0.7, 0.6, 0.9, 0.95], 0.01) == 641`.
223
+
224
+ To draw that sample from a classified raster:
225
+
226
+ ```python
227
+ mask = ss.RasterMask(landcover_array, origin=(x_min, y_max), cell_size=30, nodata=0)
228
+ n_h = ss.allocate_accuracy_sample(641, pixels, rare={0: 75, 1: 75}) # fix rare classes
229
+ validation = ss.stratified_pixels(mask, dict(zip(mask.class_counts(), n_h)), seed=7)
230
+ validation.to_csv("validation_points.csv") # x, y, row, col, stratum, pi, weight
231
+ ```
232
+
233
+ ## Command line
234
+
235
+ ```bash
236
+ # 50 GRTS sites plus 10 replacements
237
+ spatialsample grts area.geojson --n 50 --over 10 --seed 1 -o sites.geojson
238
+
239
+ # other designs: srs, systematic (--shape hex --random-rotation), bas, lpm, poisson,
240
+ # stratified (--field zone --allocation proportional --method grts)
241
+ spatialsample bas area.geojson --n 40 --over 5 --seed 3 -o sites.csv
242
+
243
+ # spatial balance of any set of sites (GeoJSON points or CSV with x,y)
244
+ spatialsample balance sites.geojson --frame area.geojson
245
+
246
+ # compare designs on your own study area
247
+ spatialsample compare area.geojson --n 50 --reps 50
248
+
249
+ # Olofsson accuracy assessment from an error matrix CSV (header row and label column optional)
250
+ spatialsample olofsson matrix.csv --areas 200000,150000,3200000,6450000 --pixel-area 0.09
251
+ spatialsample samplesize --areas 200000,150000,3200000,6450000 --users 0.7,0.6,0.9,0.95 --target-se 0.01
252
+ ```
253
+
254
+ Output is GeoJSON (default, also to stdout when `-o` is omitted) or CSV by file extension.
255
+ Each site carries `site_id`, `design`, `site_type`, `order`, `pi` and `weight`.
256
+
257
+ ## API overview
258
+
259
+ | Area | Names |
260
+ |---|---|
261
+ | Domains | `Region`, `Polygon`, `RasterMask` |
262
+ | Designs | `srs`, `systematic`, `stratified`, `stratified_pixels`, `grts`, `bas`, `lpm`, `poisson_disk`, `cluster`, `two_stage`, `points_along_lines`, `random_transects` |
263
+ | Building blocks | `inclusion_probabilities`, `allocate`, `grts_order`, `reverse_hierarchical_order`, `halton`, `lpm2_select` |
264
+ | Estimation | `ht_total`, `ht_mean`, `srs_variance`, `local_variance`, `local_weights`, `estimate`, `Estimate` |
265
+ | Balance | `voronoi_balance`, `spatial_balance`, `compare_designs` |
266
+ | Accuracy | `olofsson`, `AccuracyAssessment`, `sample_size`, `allocate_accuracy_sample` |
267
+ | I/O | `Sample.to_geojson`, `Sample.to_csv`, `Sample.to_geodataframe`, `read_region`, `read_strata`, `read_points`, `read_lines`, `read_matrix_csv` |
268
+
269
+ ## Development and testing
270
+
271
+ ```bash
272
+ pip install -e ".[dev,all]"
273
+ pytest --cov=spatialsample
274
+ ruff check . && ruff format --check src tests && mypy src
275
+ ```
276
+
277
+ The test suite includes Monte Carlo checks: empirical inclusion frequencies of GRTS and LPM
278
+ match unequal target probabilities, Horvitz-Thompson estimates are unbiased for finite and
279
+ continuous designs, GRTS/BAS/LPM are better balanced than simple random sampling, holes are
280
+ never sampled, and every design is reproducible with a seed.
281
+
282
+ ## References
283
+
284
+ - Cochran, W. G. (1977). *Sampling Techniques* (3rd ed.). Wiley.
285
+ - Cordy, C. B. (1993). An extension of the Horvitz-Thompson theorem to point sampling from
286
+ a continuous universe. *Statistics & Probability Letters*, 18(5), 353-362.
287
+ - Dumelle, M., Kincaid, T., Olsen, A. R., & Weber, M. (2023). spsurvey: Spatial sampling
288
+ design and analysis in R. *Journal of Statistical Software*, 105(3), 1-29.
289
+ - Grafström, A., Lundström, N. L. P., & Schelin, L. (2012). Spatially balanced sampling
290
+ through the pivotal method. *Biometrics*, 68(2), 514-520.
291
+ - Horvitz, D. G., & Thompson, D. J. (1952). A generalization of sampling without
292
+ replacement from a finite universe. *Journal of the American Statistical Association*,
293
+ 47(260), 663-685.
294
+ - Olofsson, P., Foody, G. M., Herold, M., Stehman, S. V., Woodcock, C. E., & Wulder, M. A.
295
+ (2014). Good practices for estimating area and assessing accuracy of land change.
296
+ *Remote Sensing of Environment*, 148, 42-57.
297
+ - Robertson, B. L., Brown, J. A., McDonald, T., & Jaksons, P. (2013). BAS: Balanced
298
+ acceptance sampling of natural resources. *Biometrics*, 69(3), 776-784.
299
+ - Stevens, D. L., & Olsen, A. R. (2003). Variance estimation for spatially balanced samples
300
+ of environmental resources. *Environmetrics*, 14(6), 593-610.
301
+ - Stevens, D. L., & Olsen, A. R. (2004). Spatially balanced sampling of natural resources.
302
+ *Journal of the American Statistical Association*, 99(465), 262-278.
303
+ - Tillé, Y. (2006). *Sampling Algorithms*. Springer.
304
+
305
+ ## License
306
+
307
+ MIT, see [LICENSE](https://github.com/YusufEminoglu/spatialsample_sdk/blob/main/LICENSE).
308
+ If you use spatialsample in research, please cite it using
309
+ [CITATION.cff](https://github.com/YusufEminoglu/spatialsample_sdk/blob/main/CITATION.cff).
@@ -0,0 +1,262 @@
1
+ # spatialsample
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/spatialsample.svg?color=10b981)](https://pypi.org/project/spatialsample/)
4
+ [![Python version support](https://img.shields.io/pypi/pyversions/spatialsample.svg?color=3b82f6)](https://pypi.org/project/spatialsample/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/YusufEminoglu/spatialsample_sdk/blob/main/LICENSE)
6
+ [![Code style: Ruff](https://img.shields.io/badge/code%20style-ruff-D7FF64.svg)](https://docs.astral.sh/ruff/)
7
+
8
+ **Design-based spatial sampling in Python.** Decide where to put field plots, household
9
+ survey points or validation samples for a land-cover map, with spatially balanced designs
10
+ (GRTS, BAS, LPM), classical designs, exact inclusion probabilities and design weights,
11
+ Horvitz-Thompson estimation, a spatial-balance metric to compare designs, and the
12
+ Olofsson et al. (2014) good-practice workflow for map accuracy and area estimation.
13
+
14
+ The core needs only NumPy. SciPy (faster nearest-neighbour search), Shapely and GeoPandas
15
+ are optional.
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ pip install spatialsample # NumPy only
21
+ pip install "spatialsample[scipy]" # + SciPy KD-tree for large balance/frame computations
22
+ pip install "spatialsample[geo]" # + Shapely and GeoPandas interoperability
23
+ pip install "spatialsample[all]" # everything
24
+ ```
25
+
26
+ ## Quick start
27
+
28
+ ```python
29
+ import spatialsample as ss
30
+
31
+ # Study area in a projected CRS (metres): an L-shaped block with a lake (hole).
32
+ area = ss.Region.from_coords(
33
+ [(0, 0), (1000, 0), (1000, 600), (600, 600), (600, 1000), (0, 1000)],
34
+ holes=[[(200, 200), (400, 200), (400, 400), (200, 400)]],
35
+ )
36
+
37
+ sites = ss.grts(area, n=50, n_over=10, seed=1) # 50 base + 10 replacement sites
38
+ sites.to_geojson("sites.geojson") # or .to_csv(), .to_geodataframe()
39
+
40
+ print(sites.base().weight.sum()) # 800000.0 (= area; weights in m^2)
41
+ print(ss.spatial_balance(sites, domain=area).value) # Voronoi balance, lower is better
42
+
43
+ # After the field work: estimate the mean of a variable y observed at the base sites
44
+ base = sites.base()
45
+ y = observe(base.xy) # your measurements
46
+ est = ss.estimate(base, y) # HT total, ratio mean, local variance
47
+ print(est.mean, est.mean_se)
48
+ ```
49
+
50
+ Inputs can be a `Region`, a `RasterMask`, any GeoJSON mapping (`Polygon`, `MultiPolygon`,
51
+ `Feature`, `FeatureCollection`) or any object with `__geo_interface__` (Shapely, GeoPandas).
52
+ GRTS and LPM also accept an `(N, 2)` array of candidate sites (a finite frame, such as
53
+ households, lakes or pixels), optionally with a size measure `prob=` for
54
+ unequal-probability sampling.
55
+
56
+ > Coordinates are treated as planar. Reproject longitude/latitude data to a projected or
57
+ > equal-area CRS before sampling; distance-based designs and the balance metric assume
58
+ > linear units.
59
+
60
+ ## Designs
61
+
62
+ | Function | Design | Inclusion probabilities | Reference |
63
+ |---|---|---|---|
64
+ | `srs` | Simple random sampling of locations | exact, `n / area` | |
65
+ | `systematic` | Square or hexagonal grid, uniformly random start, optional random rotation | exact, `1 / cell area` (random `n`) | |
66
+ | `stratified` | Stratified by polygons or raster classes; proportional, equal or Neyman allocation; SRS, BAS, GRTS or LPM within strata | exact, `n_h / area_h` | Cochran (1977) |
67
+ | `stratified_pixels` | Stratified random sampling of raster pixels without replacement | exact, `n_h / N_h` | Olofsson et al. (2014) |
68
+ | `grts` | Generalized Random Tessellation Stratified: randomised quadrant-recursive addresses, systematic selection on the resulting line, reverse hierarchical ordering, oversample sites | exact (finite frames), equal or unequal | Stevens & Olsen (2004) |
69
+ | `bas` | Balanced Acceptance Sampling from a random-start Halton sequence (bases 2, 3); oversample continues the sequence | exact, `n / area` | Robertson et al. (2013) |
70
+ | `lpm` | Local pivotal method (LPM2), equal or unequal probabilities | exact | Grafström et al. (2012) |
71
+ | `poisson_disk` | Minimum-distance (simple sequential inhibition) sampling | approximate, `n / area` | |
72
+ | `cluster` | Cluster plots (FIA-style centre + 3 at 120 degrees, 2 x 2, line, cross or custom) around SRS/BAS/GRTS centres | `n_clusters / area` per subplot | |
73
+ | `two_stage` | PPS-by-area (or equal) selection of primary units, then SRS inside each | exact; self-weighting with PPS | |
74
+ | `points_along_lines` | Systematic points along roads/rivers/hedgerows with random start | exact, `1 / spacing` per unit length | |
75
+ | `random_transects` | Straight transects of fixed length and random (or fixed) bearing | `n / area` | |
76
+
77
+ Every design returns a `Sample` with `xy`, `pi` (inclusion probability for finite frames,
78
+ inclusion density per unit area for continuous designs, following Cordy 1993), `weight =
79
+ 1/pi`, `site_type` (`base` / `over`), visiting `order`, optional `stratum`, `index` and
80
+ extra columns (`cluster`, `psu`, `chainage`, pixel `row`/`col`, ...).
81
+
82
+ GRTS and LPM on a continuous area use a dense, randomly offset grid frame (default
83
+ `max(4000, 100 (n + n_over))` cells for GRTS) and move each selected cell centre to a
84
+ uniform location within its cell, as recent versions of the R package spsurvey do.
85
+
86
+ ### Which design is most spread out?
87
+
88
+ Voronoi spatial balance (Stevens & Olsen 2004): the variance of the summed inclusion
89
+ probability inside each site's Voronoi polygon, `mean((v_i - 1)^2)`. Zero is perfectly
90
+ balanced. Measured with `ss.compare_designs` on the L-shaped area above (800,000 m^2, with
91
+ a hole), `n = 50`, 100 independent draws per design, 20,000-point evaluation grid:
92
+
93
+ | Design | Voronoi balance, mean | sd |
94
+ |---|---:|---:|
95
+ | systematic, square (random rotation) | 0.041 | 0.011 |
96
+ | systematic, hexagonal (random rotation) | 0.044 | 0.008 |
97
+ | LPM | 0.074 | 0.017 |
98
+ | BAS | 0.087 | 0.021 |
99
+ | GRTS | 0.105 | 0.024 |
100
+ | Poisson-disk (default distance) | 0.118 | 0.033 |
101
+ | simple random | 0.352 | 0.106 |
102
+
103
+ Systematic grids are the most even but have a random sample size, no valid design-based
104
+ variance estimator and are vulnerable to periodic patterns; GRTS, BAS and LPM give fixed
105
+ sample sizes, exact inclusion probabilities and three to five times better balance than
106
+ simple random sampling. Reproduce with `spatialsample compare area.geojson --n 50 --reps 100`.
107
+
108
+ ### Does balance pay off?
109
+
110
+ Estimating the mean of the smooth surface `10 + 0.01 x + 5 sin(y / 150)` over the same area
111
+ with `n = 30` (400 draws per design, ratio estimator, `ss.estimate` defaults):
112
+
113
+ | Design | bias | true SD of the estimate | mean estimated SE |
114
+ |---|---:|---:|---:|
115
+ | simple random | +0.026 | 0.905 | 0.880 (SRS variance) |
116
+ | GRTS | -0.015 | 0.331 | 0.399 (local variance) |
117
+ | LPM | -0.026 | 0.282 | 0.423 (local variance) |
118
+ | BAS | +0.011 | 0.180 | 0.426 (local variance) |
119
+ | systematic, hexagonal | -0.003 | 0.185 | 0.456 (local variance) |
120
+
121
+ All biases are within Monte Carlo error. Spatially balanced designs cut the standard error
122
+ by a factor of 2.7 (GRTS) to 5 (BAS) here; the local neighbourhood variance estimator captures much of that
123
+ gain while staying on the conservative side for smooth surfaces.
124
+
125
+ ## Estimation
126
+
127
+ ```python
128
+ ss.ht_total(y, pi) # Horvitz-Thompson total (integral for continuous designs)
129
+ ss.ht_mean(y, pi, size=area) # HT mean; without size: Hajek ratio mean
130
+ ss.srs_variance(y, pi) # with-replacement variance of the HT total
131
+ ss.local_variance(y, xy, pi, k=4) # Stevens & Olsen (2003) neighbourhood variance
132
+ ss.estimate(sample, y) # all of the above with a CI, design-aware defaults
133
+ ```
134
+
135
+ `local_variance` uses neighbourhoods of each site and its three nearest sample neighbours,
136
+ initial weights proportional to `1/pi` balanced to a doubly stochastic matrix by
137
+ iterative proportional fitting. By default each neighbourhood term is divided by
138
+ `1 - sum_j w_ij^2`, the usual small-sample correction for a weighted variance, which makes
139
+ the estimator unbiased (relative to the with-replacement variance) when the variable has no
140
+ spatial structure; pass `correction=False` for the uncorrected form, which is about 25 %
141
+ smaller with `k = 4`. Tests check both properties by simulation.
142
+
143
+ ## Map accuracy and area estimation (Olofsson et al. 2014)
144
+
145
+ ```python
146
+ import spatialsample as ss
147
+
148
+ # Section 5 worked example of Olofsson et al. (2014): sample counts, rows = map class,
149
+ # columns = reference class; mapped areas in 30 m pixels (0.09 ha each).
150
+ matrix = [[66, 0, 5, 4],
151
+ [0, 55, 8, 12],
152
+ [1, 0, 153, 11],
153
+ [2, 1, 9, 313]]
154
+ pixels = [200_000, 150_000, 3_200_000, 6_450_000]
155
+ labels = ["Deforestation", "Forest gain", "Stable forest", "Stable non-forest"]
156
+
157
+ res = ss.olofsson(matrix, [p * 0.09 for p in pixels], labels=labels)
158
+ print(res.summary())
159
+ ```
160
+
161
+ ```
162
+ class mapped area est. area +/- 95% CI user +/- producer +/-
163
+ -------------------------------------------------------------------------------------------------
164
+ Deforestation 18,000 21,158 6,158 0.880 0.074 0.749 0.213
165
+ Forest gain 13,500 11,686 3,756 0.733 0.101 0.847 0.254
166
+ Stable forest 288,000 285,770 15,510 0.927 0.040 0.935 0.034
167
+ Stable non-forest 580,500 581,386 16,281 0.963 0.021 0.962 0.018
168
+ overall accuracy 0.947 +/- 0.018 (95% CI), n = 640
169
+ ```
170
+
171
+ These reproduce the published estimates (for example a deforestation area of
172
+ 21,158 ha +/- 6,158 ha against 18,000 ha mapped; user's accuracy 0.88, producer's accuracy
173
+ 0.75 +/- 0.21; overall accuracy 0.95). The paper's sample-size example (conjectured user's
174
+ accuracies 0.7, 0.6, 0.9, 0.95 and a target standard error of 0.01 for overall accuracy) is
175
+ `ss.sample_size(pixels, [0.7, 0.6, 0.9, 0.95], 0.01) == 641`.
176
+
177
+ To draw that sample from a classified raster:
178
+
179
+ ```python
180
+ mask = ss.RasterMask(landcover_array, origin=(x_min, y_max), cell_size=30, nodata=0)
181
+ n_h = ss.allocate_accuracy_sample(641, pixels, rare={0: 75, 1: 75}) # fix rare classes
182
+ validation = ss.stratified_pixels(mask, dict(zip(mask.class_counts(), n_h)), seed=7)
183
+ validation.to_csv("validation_points.csv") # x, y, row, col, stratum, pi, weight
184
+ ```
185
+
186
+ ## Command line
187
+
188
+ ```bash
189
+ # 50 GRTS sites plus 10 replacements
190
+ spatialsample grts area.geojson --n 50 --over 10 --seed 1 -o sites.geojson
191
+
192
+ # other designs: srs, systematic (--shape hex --random-rotation), bas, lpm, poisson,
193
+ # stratified (--field zone --allocation proportional --method grts)
194
+ spatialsample bas area.geojson --n 40 --over 5 --seed 3 -o sites.csv
195
+
196
+ # spatial balance of any set of sites (GeoJSON points or CSV with x,y)
197
+ spatialsample balance sites.geojson --frame area.geojson
198
+
199
+ # compare designs on your own study area
200
+ spatialsample compare area.geojson --n 50 --reps 50
201
+
202
+ # Olofsson accuracy assessment from an error matrix CSV (header row and label column optional)
203
+ spatialsample olofsson matrix.csv --areas 200000,150000,3200000,6450000 --pixel-area 0.09
204
+ spatialsample samplesize --areas 200000,150000,3200000,6450000 --users 0.7,0.6,0.9,0.95 --target-se 0.01
205
+ ```
206
+
207
+ Output is GeoJSON (default, also to stdout when `-o` is omitted) or CSV by file extension.
208
+ Each site carries `site_id`, `design`, `site_type`, `order`, `pi` and `weight`.
209
+
210
+ ## API overview
211
+
212
+ | Area | Names |
213
+ |---|---|
214
+ | Domains | `Region`, `Polygon`, `RasterMask` |
215
+ | Designs | `srs`, `systematic`, `stratified`, `stratified_pixels`, `grts`, `bas`, `lpm`, `poisson_disk`, `cluster`, `two_stage`, `points_along_lines`, `random_transects` |
216
+ | Building blocks | `inclusion_probabilities`, `allocate`, `grts_order`, `reverse_hierarchical_order`, `halton`, `lpm2_select` |
217
+ | Estimation | `ht_total`, `ht_mean`, `srs_variance`, `local_variance`, `local_weights`, `estimate`, `Estimate` |
218
+ | Balance | `voronoi_balance`, `spatial_balance`, `compare_designs` |
219
+ | Accuracy | `olofsson`, `AccuracyAssessment`, `sample_size`, `allocate_accuracy_sample` |
220
+ | I/O | `Sample.to_geojson`, `Sample.to_csv`, `Sample.to_geodataframe`, `read_region`, `read_strata`, `read_points`, `read_lines`, `read_matrix_csv` |
221
+
222
+ ## Development and testing
223
+
224
+ ```bash
225
+ pip install -e ".[dev,all]"
226
+ pytest --cov=spatialsample
227
+ ruff check . && ruff format --check src tests && mypy src
228
+ ```
229
+
230
+ The test suite includes Monte Carlo checks: empirical inclusion frequencies of GRTS and LPM
231
+ match unequal target probabilities, Horvitz-Thompson estimates are unbiased for finite and
232
+ continuous designs, GRTS/BAS/LPM are better balanced than simple random sampling, holes are
233
+ never sampled, and every design is reproducible with a seed.
234
+
235
+ ## References
236
+
237
+ - Cochran, W. G. (1977). *Sampling Techniques* (3rd ed.). Wiley.
238
+ - Cordy, C. B. (1993). An extension of the Horvitz-Thompson theorem to point sampling from
239
+ a continuous universe. *Statistics & Probability Letters*, 18(5), 353-362.
240
+ - Dumelle, M., Kincaid, T., Olsen, A. R., & Weber, M. (2023). spsurvey: Spatial sampling
241
+ design and analysis in R. *Journal of Statistical Software*, 105(3), 1-29.
242
+ - Grafström, A., Lundström, N. L. P., & Schelin, L. (2012). Spatially balanced sampling
243
+ through the pivotal method. *Biometrics*, 68(2), 514-520.
244
+ - Horvitz, D. G., & Thompson, D. J. (1952). A generalization of sampling without
245
+ replacement from a finite universe. *Journal of the American Statistical Association*,
246
+ 47(260), 663-685.
247
+ - Olofsson, P., Foody, G. M., Herold, M., Stehman, S. V., Woodcock, C. E., & Wulder, M. A.
248
+ (2014). Good practices for estimating area and assessing accuracy of land change.
249
+ *Remote Sensing of Environment*, 148, 42-57.
250
+ - Robertson, B. L., Brown, J. A., McDonald, T., & Jaksons, P. (2013). BAS: Balanced
251
+ acceptance sampling of natural resources. *Biometrics*, 69(3), 776-784.
252
+ - Stevens, D. L., & Olsen, A. R. (2003). Variance estimation for spatially balanced samples
253
+ of environmental resources. *Environmetrics*, 14(6), 593-610.
254
+ - Stevens, D. L., & Olsen, A. R. (2004). Spatially balanced sampling of natural resources.
255
+ *Journal of the American Statistical Association*, 99(465), 262-278.
256
+ - Tillé, Y. (2006). *Sampling Algorithms*. Springer.
257
+
258
+ ## License
259
+
260
+ MIT, see [LICENSE](https://github.com/YusufEminoglu/spatialsample_sdk/blob/main/LICENSE).
261
+ If you use spatialsample in research, please cite it using
262
+ [CITATION.cff](https://github.com/YusufEminoglu/spatialsample_sdk/blob/main/CITATION.cff).