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.
- spatialsample/__init__.py +89 -0
- spatialsample/_frame.py +91 -0
- spatialsample/_types.py +36 -0
- spatialsample/accuracy.py +232 -0
- spatialsample/balance.py +144 -0
- spatialsample/bas.py +96 -0
- spatialsample/cli.py +184 -0
- spatialsample/designs.py +562 -0
- spatialsample/estimators.py +222 -0
- spatialsample/geometry.py +253 -0
- spatialsample/grts.py +175 -0
- spatialsample/io.py +161 -0
- spatialsample/lpm.py +133 -0
- spatialsample/probabilities.py +202 -0
- spatialsample/py.typed +0 -0
- spatialsample/raster.py +170 -0
- spatialsample/sample.py +213 -0
- spatialsample-0.1.0.dist-info/METADATA +309 -0
- spatialsample-0.1.0.dist-info/RECORD +23 -0
- spatialsample-0.1.0.dist-info/WHEEL +5 -0
- spatialsample-0.1.0.dist-info/entry_points.txt +2 -0
- spatialsample-0.1.0.dist-info/licenses/LICENSE +21 -0
- spatialsample-0.1.0.dist-info/top_level.txt +1 -0
spatialsample/cli.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Command-line interface: ``spatialsample <command> ...``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Any, List, Optional, Sequence
|
|
9
|
+
|
|
10
|
+
from . import __version__
|
|
11
|
+
from .accuracy import olofsson, sample_size
|
|
12
|
+
from .balance import compare_designs, spatial_balance
|
|
13
|
+
from .bas import bas
|
|
14
|
+
from .designs import poisson_disk, srs, stratified, systematic
|
|
15
|
+
from .grts import grts
|
|
16
|
+
from .io import read_matrix_csv, read_points, read_region, read_strata
|
|
17
|
+
from .lpm import lpm
|
|
18
|
+
from .sample import Sample
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _floats(s: str) -> List[float]:
|
|
22
|
+
return [float(v) for v in s.replace(";", ",").split(",") if v.strip()]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _write(sample: Sample, out: Optional[str], lines: bool = False) -> None:
|
|
26
|
+
if out is None:
|
|
27
|
+
json.dump(sample.to_geojson(include_lines=lines), sys.stdout)
|
|
28
|
+
sys.stdout.write("\n")
|
|
29
|
+
return
|
|
30
|
+
if out.lower().endswith(".csv"):
|
|
31
|
+
sample.to_csv(out)
|
|
32
|
+
else:
|
|
33
|
+
sample.to_geojson(out, include_lines=lines)
|
|
34
|
+
nb = len(sample.base())
|
|
35
|
+
print(f"{sample.design}: {nb} base + {len(sample) - nb} over sites -> {out}", file=sys.stderr)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _common(p: argparse.ArgumentParser, over: bool = False) -> None:
|
|
39
|
+
p.add_argument("area", help="polygon GeoJSON file (projected coordinates)")
|
|
40
|
+
p.add_argument("--n", type=int, required=True, help="base sample size")
|
|
41
|
+
if over:
|
|
42
|
+
p.add_argument("--over", type=int, default=0, help="oversample (replacement) sites")
|
|
43
|
+
p.add_argument("--seed", type=int, default=None, help="random seed")
|
|
44
|
+
p.add_argument("-o", "--output", default=None, help="output .geojson or .csv (default: stdout)")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
48
|
+
ap = argparse.ArgumentParser(
|
|
49
|
+
prog="spatialsample",
|
|
50
|
+
description="Design-based spatial sampling: GRTS, BAS, LPM, stratified, "
|
|
51
|
+
"systematic designs, spatial balance and map accuracy assessment.",
|
|
52
|
+
)
|
|
53
|
+
ap.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
54
|
+
sub = ap.add_subparsers(dest="command", required=True)
|
|
55
|
+
|
|
56
|
+
_common(sub.add_parser("srs", help="simple random sample"))
|
|
57
|
+
p = sub.add_parser("systematic", help="systematic square or hexagonal grid")
|
|
58
|
+
_common(p)
|
|
59
|
+
p.add_argument("--shape", choices=["square", "hex"], default="square")
|
|
60
|
+
p.add_argument("--random-rotation", action="store_true")
|
|
61
|
+
_common(sub.add_parser("grts", help="GRTS spatially balanced sample"), over=True)
|
|
62
|
+
_common(sub.add_parser("bas", help="balanced acceptance sample (Halton)"), over=True)
|
|
63
|
+
p = sub.add_parser("lpm", help="local pivotal method")
|
|
64
|
+
_common(p)
|
|
65
|
+
p.add_argument("--frame-size", type=int, default=None)
|
|
66
|
+
p = sub.add_parser("poisson", help="minimum-distance (Poisson-disk) sample")
|
|
67
|
+
_common(p)
|
|
68
|
+
p.add_argument("--min-dist", type=float, default=None)
|
|
69
|
+
p = sub.add_parser("stratified", help="stratified sample by a polygon attribute")
|
|
70
|
+
_common(p)
|
|
71
|
+
p.add_argument("--field", required=True, help="property holding the stratum label")
|
|
72
|
+
p.add_argument("--allocation", choices=["proportional", "equal"], default="proportional")
|
|
73
|
+
p.add_argument("--method", choices=["srs", "bas", "grts", "lpm"], default="srs")
|
|
74
|
+
|
|
75
|
+
p = sub.add_parser("balance", help="Voronoi spatial balance of a set of sites")
|
|
76
|
+
p.add_argument("sites", help="sites (GeoJSON points or CSV with x,y)")
|
|
77
|
+
p.add_argument("--frame", required=True, help="polygon GeoJSON of the sampled area")
|
|
78
|
+
p.add_argument("--frame-size", type=int, default=20000)
|
|
79
|
+
p.add_argument("--all-sites", action="store_true", help="include oversample sites")
|
|
80
|
+
p.add_argument("--json", action="store_true")
|
|
81
|
+
|
|
82
|
+
p = sub.add_parser("compare", help="compare the spatial balance of designs")
|
|
83
|
+
p.add_argument("area")
|
|
84
|
+
p.add_argument("--n", type=int, required=True)
|
|
85
|
+
p.add_argument("--reps", type=int, default=20)
|
|
86
|
+
p.add_argument("--seed", type=int, default=0)
|
|
87
|
+
p.add_argument("--json", action="store_true")
|
|
88
|
+
|
|
89
|
+
p = sub.add_parser("olofsson", help="accuracy and area estimates from an error matrix")
|
|
90
|
+
p.add_argument("matrix", help="CSV of sample counts (rows map, columns reference)")
|
|
91
|
+
p.add_argument(
|
|
92
|
+
"--areas", required=True, help="mapped area/pixels per map class, comma separated"
|
|
93
|
+
)
|
|
94
|
+
p.add_argument(
|
|
95
|
+
"--pixel-area", type=float, default=1.0, help="multiply areas by this (e.g. 0.09 ha)"
|
|
96
|
+
)
|
|
97
|
+
p.add_argument("--labels", default=None, help="comma-separated class names")
|
|
98
|
+
p.add_argument("--conf", type=float, default=0.95)
|
|
99
|
+
p.add_argument("--json", action="store_true")
|
|
100
|
+
|
|
101
|
+
p = sub.add_parser("samplesize", help="sample size for a target SE of overall accuracy")
|
|
102
|
+
p.add_argument("--areas", required=True)
|
|
103
|
+
p.add_argument("--users", required=True, help="conjectured user's accuracies")
|
|
104
|
+
p.add_argument("--target-se", type=float, default=0.01)
|
|
105
|
+
return ap
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
109
|
+
args = build_parser().parse_args(argv)
|
|
110
|
+
cmd = args.command
|
|
111
|
+
try:
|
|
112
|
+
return _run(cmd, args)
|
|
113
|
+
except (ValueError, KeyError, OSError) as exc:
|
|
114
|
+
print(f"spatialsample {cmd}: error: {exc}", file=sys.stderr)
|
|
115
|
+
return 2
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _run(cmd: str, args: Any) -> int:
|
|
119
|
+
if cmd in ("srs", "systematic", "grts", "bas", "lpm", "poisson", "stratified"):
|
|
120
|
+
if cmd == "stratified":
|
|
121
|
+
strata = read_strata(args.area, args.field)
|
|
122
|
+
s = stratified(
|
|
123
|
+
strata, args.n, allocation=args.allocation, method=args.method, seed=args.seed
|
|
124
|
+
)
|
|
125
|
+
else:
|
|
126
|
+
region = read_region(args.area)
|
|
127
|
+
if cmd == "srs":
|
|
128
|
+
s = srs(region, args.n, seed=args.seed)
|
|
129
|
+
elif cmd == "systematic":
|
|
130
|
+
rot = "random" if args.random_rotation else 0.0
|
|
131
|
+
s = systematic(region, args.n, shape=args.shape, rotation=rot, seed=args.seed)
|
|
132
|
+
elif cmd == "grts":
|
|
133
|
+
s = grts(region, args.n, n_over=args.over, seed=args.seed)
|
|
134
|
+
elif cmd == "bas":
|
|
135
|
+
s = bas(region, args.n, n_over=args.over, seed=args.seed)
|
|
136
|
+
elif cmd == "lpm":
|
|
137
|
+
s = lpm(region, args.n, frame_size=args.frame_size, seed=args.seed)
|
|
138
|
+
else:
|
|
139
|
+
s = poisson_disk(region, args.n, min_dist=args.min_dist, seed=args.seed)
|
|
140
|
+
_write(s, args.output)
|
|
141
|
+
return 0
|
|
142
|
+
if cmd == "balance":
|
|
143
|
+
xy, props = read_points(args.sites)
|
|
144
|
+
if not args.all_sites and props and any("site_type" in p for p in props):
|
|
145
|
+
keep = [i for i, p in enumerate(props) if p.get("site_type", "base") == "base"]
|
|
146
|
+
xy = xy[keep]
|
|
147
|
+
res = spatial_balance(xy, domain=read_region(args.frame), frame_size=args.frame_size)
|
|
148
|
+
if args.json:
|
|
149
|
+
print(
|
|
150
|
+
json.dumps(
|
|
151
|
+
{"n": res.n, "voronoi_balance": res.value, "max_deviation": res.max_deviation}
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
else:
|
|
155
|
+
print(f"sites: {res.n}")
|
|
156
|
+
print(f"Voronoi spatial balance (lower is better): {res.value:.4f}")
|
|
157
|
+
print(f"largest |v_i - 1|: {res.max_deviation:.4f}")
|
|
158
|
+
return 0
|
|
159
|
+
if cmd == "compare":
|
|
160
|
+
cmp = compare_designs(read_region(args.area), args.n, reps=args.reps, seed=args.seed)
|
|
161
|
+
if args.json:
|
|
162
|
+
print(json.dumps(cmp, indent=2))
|
|
163
|
+
else:
|
|
164
|
+
print(f"{'design':<20}{'balance':>10}{'sd':>10}{'mean n':>10}")
|
|
165
|
+
for k, v in sorted(cmp.items(), key=lambda kv: kv[1]["mean"]):
|
|
166
|
+
print(f"{k:<20}{v['mean']:>10.4f}{v['sd']:>10.4f}{v['mean_n']:>10.1f}")
|
|
167
|
+
return 0
|
|
168
|
+
if cmd == "olofsson":
|
|
169
|
+
mat, labels = read_matrix_csv(args.matrix)
|
|
170
|
+
if args.labels:
|
|
171
|
+
labels = [s.strip() for s in args.labels.split(",")]
|
|
172
|
+
areas = [a * args.pixel_area for a in _floats(args.areas)]
|
|
173
|
+
acc = olofsson(mat, areas, labels=labels, conf=args.conf)
|
|
174
|
+
print(json.dumps(acc.as_dict(), indent=2) if args.json else acc.summary())
|
|
175
|
+
return 0
|
|
176
|
+
if cmd == "samplesize":
|
|
177
|
+
n = sample_size(_floats(args.areas), _floats(args.users), args.target_se)
|
|
178
|
+
print(n)
|
|
179
|
+
return 0
|
|
180
|
+
raise AssertionError(cmd) # pragma: no cover
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
if __name__ == "__main__": # pragma: no cover
|
|
184
|
+
sys.exit(main())
|