spatialsample 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,213 @@
1
+ """The :class:`Sample` container returned by every design."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import csv
6
+ import json
7
+ import os
8
+ from dataclasses import dataclass, field
9
+ from typing import Any, Dict, List, Optional, Union
10
+
11
+ import numpy as np
12
+
13
+ from ._types import FloatArray
14
+
15
+ __all__ = ["Sample"]
16
+
17
+ PathLike = Union[str, "os.PathLike[str]"]
18
+
19
+
20
+ @dataclass
21
+ class Sample:
22
+ """Selected sites with their inclusion probabilities and design weights.
23
+
24
+ Attributes
25
+ ----------
26
+ xy:
27
+ ``(n, 2)`` site coordinates.
28
+ pi:
29
+ First-order inclusion probability of each site. For designs over a
30
+ finite frame (points, pixels, PSUs) this is a probability in ``(0, 1]``.
31
+ For continuous designs over an area it is an inclusion *density*
32
+ (expected number of sites per unit area), as in Cordy (1993).
33
+ weight:
34
+ Design weight ``1 / pi``: the number of frame units (finite) or the
35
+ area (continuous) each site represents.
36
+ design:
37
+ Short name of the design that produced the sample.
38
+ kind:
39
+ ``"finite"`` or ``"continuous"``.
40
+ site_type:
41
+ ``"base"`` or ``"over"`` (oversample / replacement sites).
42
+ order:
43
+ Visiting order. For GRTS and BAS, any initial run of base-then-over
44
+ sites in this order is itself spatially balanced.
45
+ stratum:
46
+ Stratum label per site, or ``None``.
47
+ index:
48
+ Index into the finite frame, or ``None``.
49
+ attrs:
50
+ Extra per-site columns (cluster id, chainage, pixel row/col, ...).
51
+ meta:
52
+ Design parameters such as ``n``, ``seed``, ``area`` and ``population_size``.
53
+ lines:
54
+ Optional line geometries (transect designs).
55
+ """
56
+
57
+ xy: FloatArray
58
+ pi: FloatArray
59
+ weight: FloatArray
60
+ design: str
61
+ kind: str = "continuous"
62
+ site_type: Optional[np.ndarray] = None
63
+ order: Optional[np.ndarray] = None
64
+ stratum: Optional[np.ndarray] = None
65
+ index: Optional[np.ndarray] = None
66
+ attrs: Dict[str, np.ndarray] = field(default_factory=dict)
67
+ meta: Dict[str, Any] = field(default_factory=dict)
68
+ lines: Optional[List[FloatArray]] = None
69
+
70
+ def __post_init__(self) -> None:
71
+ self.xy = np.asarray(self.xy, dtype=float).reshape(-1, 2)
72
+ n = len(self.xy)
73
+ self.pi = np.broadcast_to(np.asarray(self.pi, dtype=float), (n,)).copy()
74
+ self.weight = np.broadcast_to(np.asarray(self.weight, dtype=float), (n,)).copy()
75
+ if self.site_type is None:
76
+ self.site_type = np.array(["base"] * n, dtype=object)
77
+ if self.order is None:
78
+ self.order = np.arange(n)
79
+
80
+ def __len__(self) -> int:
81
+ return len(self.xy)
82
+
83
+ # ------------------------------------------------------------------ subsets
84
+ def _subset(self, mask: np.ndarray) -> Sample:
85
+ def cut(a: Optional[np.ndarray]) -> Optional[np.ndarray]:
86
+ return None if a is None else np.asarray(a)[mask]
87
+
88
+ return Sample(
89
+ xy=self.xy[mask],
90
+ pi=self.pi[mask],
91
+ weight=self.weight[mask],
92
+ design=self.design,
93
+ kind=self.kind,
94
+ site_type=cut(self.site_type),
95
+ order=cut(self.order),
96
+ stratum=cut(self.stratum),
97
+ index=cut(self.index),
98
+ attrs={k: np.asarray(v)[mask] for k, v in self.attrs.items()},
99
+ meta=dict(self.meta),
100
+ lines=self.lines,
101
+ )
102
+
103
+ def base(self) -> Sample:
104
+ """Only the base (non-oversample) sites."""
105
+ return self._subset(np.asarray(self.site_type) == "base")
106
+
107
+ def oversample(self) -> Sample:
108
+ """Only the oversample / replacement sites, in their recommended order."""
109
+ sub = self._subset(np.asarray(self.site_type) == "over")
110
+ return sub._subset(np.argsort(np.asarray(sub.order), kind="stable"))
111
+
112
+ def sorted(self) -> Sample:
113
+ """Sites sorted by the visiting ``order`` column."""
114
+ return self._subset(np.argsort(np.asarray(self.order), kind="stable"))
115
+
116
+ # ------------------------------------------------------------------ tabular output
117
+ def columns(self) -> Dict[str, List[Any]]:
118
+ """Column-oriented table of the sample (plain Python values)."""
119
+ n = len(self)
120
+ cols: Dict[str, List[Any]] = {
121
+ "site_id": list(range(1, n + 1)),
122
+ "x": self.xy[:, 0].tolist(),
123
+ "y": self.xy[:, 1].tolist(),
124
+ "design": [self.design] * n,
125
+ "site_type": [str(s) for s in np.asarray(self.site_type)],
126
+ "order": [int(v) for v in np.asarray(self.order)],
127
+ "pi": self.pi.tolist(),
128
+ "weight": self.weight.tolist(),
129
+ }
130
+ if self.stratum is not None:
131
+ cols["stratum"] = [_plain(v) for v in self.stratum]
132
+ if self.index is not None:
133
+ cols["frame_index"] = [int(v) for v in self.index]
134
+ for k, v in self.attrs.items():
135
+ cols[k] = [_plain(x) for x in np.asarray(v)]
136
+ return cols
137
+
138
+ def to_records(self) -> List[Dict[str, Any]]:
139
+ cols = self.columns()
140
+ keys = list(cols)
141
+ return [{k: cols[k][i] for k in keys} for i in range(len(self))]
142
+
143
+ def to_geojson(self, path: Optional[PathLike] = None, include_lines: bool = False) -> dict:
144
+ """Point FeatureCollection; written to ``path`` when given.
145
+
146
+ With ``include_lines=True`` transect lines are appended as LineString
147
+ features with ``"feature": "transect"``.
148
+ """
149
+ feats: List[Dict[str, Any]] = []
150
+ for rec in self.to_records():
151
+ x = rec.pop("x")
152
+ y = rec.pop("y")
153
+ feats.append(
154
+ {
155
+ "type": "Feature",
156
+ "geometry": {"type": "Point", "coordinates": [x, y]},
157
+ "properties": rec,
158
+ }
159
+ )
160
+ if include_lines and self.lines:
161
+ for k, line in enumerate(self.lines):
162
+ feats.append(
163
+ {
164
+ "type": "Feature",
165
+ "geometry": {
166
+ "type": "LineString",
167
+ "coordinates": np.asarray(line).tolist(),
168
+ },
169
+ "properties": {"feature": "transect", "transect": k},
170
+ }
171
+ )
172
+ fc: Dict[str, Any] = {"type": "FeatureCollection", "features": feats}
173
+ crs = self.meta.get("crs")
174
+ if crs is not None:
175
+ fc["crs"] = crs
176
+ if path is not None:
177
+ with open(path, "w", encoding="utf-8") as fh:
178
+ json.dump(fc, fh)
179
+ return fc
180
+
181
+ def to_csv(self, path: PathLike) -> None:
182
+ """Write the sample as CSV (one row per site)."""
183
+ cols = self.columns()
184
+ keys = list(cols)
185
+ with open(path, "w", newline="", encoding="utf-8") as fh:
186
+ w = csv.writer(fh)
187
+ w.writerow(keys)
188
+ for i in range(len(self)):
189
+ w.writerow([cols[k][i] for k in keys])
190
+
191
+ def to_geodataframe(self, crs: Any = None) -> Any:
192
+ """GeoPandas ``GeoDataFrame`` (requires the ``geo`` extra)."""
193
+ try:
194
+ import geopandas as gpd
195
+ except ImportError as exc: # pragma: no cover - depends on environment
196
+ raise ImportError(
197
+ "to_geodataframe needs geopandas: pip install 'spatialsample[geo]'"
198
+ ) from exc
199
+ cols = self.columns()
200
+ geom = gpd.points_from_xy(cols.pop("x"), cols.pop("y"))
201
+ return gpd.GeoDataFrame(cols, geometry=geom, crs=crs)
202
+
203
+ def __repr__(self) -> str:
204
+ nb = int(np.sum(np.asarray(self.site_type) == "base"))
205
+ return (
206
+ f"Sample(design={self.design!r}, base={nb}, over={len(self) - nb}, kind={self.kind!r})"
207
+ )
208
+
209
+
210
+ def _plain(v: Any) -> Any:
211
+ if hasattr(v, "item"):
212
+ return v.item()
213
+ return v
@@ -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,23 @@
1
+ spatialsample/__init__.py,sha256=qPpae3YQIlx5MgFtcXuOzma3gw6s8BmKIqhuuiJvHjU,2186
2
+ spatialsample/_frame.py,sha256=yy2VrLojX4A58sg4xe_6TKTJ0ibh7OJh70Dj0x202Yc,3515
3
+ spatialsample/_types.py,sha256=iUeAVJ7lkYqn0VlMSQ8Ks-BPJXLyIx4LuqrruwQB6dA,1040
4
+ spatialsample/accuracy.py,sha256=nRv0bSNuF7O-S7m77zsqZ1wfx-511RTWN1vxPPXq9co,8608
5
+ spatialsample/balance.py,sha256=53WyTWrC65USQ3dtycMpgIxEYGoe2yVaCbt8qPgSRqE,4837
6
+ spatialsample/bas.py,sha256=pD6uOgRMJgjxnxleLqDE707H_nQOlC2pHyEqsRVJEGg,3050
7
+ spatialsample/cli.py,sha256=NL3iT9H6s4fqOq_uiApvf6b1myULqniwaHY03kLOY7w,8203
8
+ spatialsample/designs.py,sha256=vHjFq0Oi2K3bQmHRavVALuGbLGRMwMXWhf48KV4dtuQ,19960
9
+ spatialsample/estimators.py,sha256=7-KnY6sDQ7bVY9YjLwzmqAZ3JyWSogImvKc0nkMA_7U,7607
10
+ spatialsample/geometry.py,sha256=aALmSM9LyW2EuUQzf4q40AZUzzQr-S1Xa9DFPfhQyBM,9486
11
+ spatialsample/grts.py,sha256=tWbqOJF95QQXn6MgoLy9unuleSWv8RzOCpZZlQfuC6k,6639
12
+ spatialsample/io.py,sha256=xyFExi5qtEt7HKJvT-lsBruOvWGgCSceVwlgeGpA6VY,5565
13
+ spatialsample/lpm.py,sha256=2nxFloNgCzI1YfNNDiDYvlj7Xuk7Gjin_ljPp10jqU0,4683
14
+ spatialsample/probabilities.py,sha256=GJClHe8nMyZZsBzbXZdzSFXn_d8_4mzaXyjos51Y4OA,7167
15
+ spatialsample/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ spatialsample/raster.py,sha256=Gfbc-eGn1R2WyrHDh1Glgk7XdrYU2UvAX0yrV8I4q90,6729
17
+ spatialsample/sample.py,sha256=2tLAr-ZQ-cTKjPFWyYwXqof2JjewrQ8pfgvfiChhQ-Y,7847
18
+ spatialsample-0.1.0.dist-info/licenses/LICENSE,sha256=Z9meOk3opsRgHOqC5mA4B8NjZoU5WU1Kqhn82FL4ICs,1072
19
+ spatialsample-0.1.0.dist-info/METADATA,sha256=1t6m0z8EYHtTruOkm-E2pi2NGheXgnaTTMPf5HNTbV8,16694
20
+ spatialsample-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
21
+ spatialsample-0.1.0.dist-info/entry_points.txt,sha256=tgrxWuPvd_fy9Kad27FIm_pmN_36Qk1wzyEJZiI-zZY,57
22
+ spatialsample-0.1.0.dist-info/top_level.txt,sha256=BwilTIkIW0liWGtughFCPzdQPxf356uVRtQb-dYmnDg,14
23
+ spatialsample-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ spatialsample = spatialsample.cli:main
@@ -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 @@
1
+ spatialsample