lsapy 0.1.0.dev0__py3-none-any.whl → 0.1.0.dev1__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.
- lsapy/__init__.py +2 -2
- lsapy/criteria.py +21 -18
- lsapy/functions.py +35 -34
- lsapy/lsa.py +223 -165
- lsapy/statistics.py +51 -43
- lsapy/utils.py +8 -12
- {lsapy-0.1.0.dev0.dist-info → lsapy-0.1.0.dev1.dist-info}/METADATA +1 -1
- lsapy-0.1.0.dev1.dist-info/RECORD +12 -0
- lsapy-0.1.0.dev0.dist-info/RECORD +0 -12
- {lsapy-0.1.0.dev0.dist-info → lsapy-0.1.0.dev1.dist-info}/WHEEL +0 -0
- {lsapy-0.1.0.dev0.dist-info → lsapy-0.1.0.dev1.dist-info}/licenses/LICENSE +0 -0
lsapy/__init__.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"""A Python package for Land Suitability Analysis."""
|
|
2
2
|
|
|
3
|
-
from lsapy.lsa import LandSuitability
|
|
4
3
|
from lsapy.criteria import SuitabilityCriteria
|
|
5
4
|
from lsapy.functions import *
|
|
5
|
+
from lsapy.lsa import LandSuitability
|
|
6
6
|
|
|
7
7
|
__author__ = "Baptiste Hamon"
|
|
8
8
|
__email__ = "baptiste.hamon@pg.canterbury.ac.nz"
|
|
9
|
-
__version__ = "0.1.0-
|
|
9
|
+
__version__ = "0.1.0-dev1"
|
lsapy/criteria.py
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
"""Suitability Criteria definition."""
|
|
2
2
|
|
|
3
|
-
from typing import Optional, Union, Any
|
|
4
3
|
import xarray as xr
|
|
5
4
|
|
|
6
|
-
from lsapy.functions import
|
|
5
|
+
from lsapy.functions import DiscreteSuitFunction, MembershipSuitFunction, SuitabilityFunction
|
|
7
6
|
|
|
8
7
|
__all__ = ["SuitabilityCriteria"]
|
|
9
8
|
|
|
9
|
+
|
|
10
10
|
class SuitabilityCriteria:
|
|
11
11
|
def __init__(
|
|
12
12
|
self,
|
|
13
13
|
name: str,
|
|
14
14
|
indicator: xr.Dataset | xr.DataArray,
|
|
15
|
-
func:
|
|
16
|
-
weight:
|
|
17
|
-
category:
|
|
18
|
-
long_name:
|
|
19
|
-
description:
|
|
15
|
+
func: SuitabilityFunction | MembershipSuitFunction | DiscreteSuitFunction,
|
|
16
|
+
weight: int | float | None = 1,
|
|
17
|
+
category: str | None = None,
|
|
18
|
+
long_name: str | None = None,
|
|
19
|
+
description: str | None = None
|
|
20
20
|
|
|
21
21
|
) -> None:
|
|
22
22
|
self.name = name
|
|
@@ -27,7 +27,7 @@ class SuitabilityCriteria:
|
|
|
27
27
|
self.long_name = long_name
|
|
28
28
|
self.description = description
|
|
29
29
|
self._from_indicator = _get_indicator_description(indicator)
|
|
30
|
-
|
|
30
|
+
|
|
31
31
|
def __repr__(self) -> str:
|
|
32
32
|
attrs = []
|
|
33
33
|
attrs.append(f"name='{self.name}'")
|
|
@@ -41,17 +41,20 @@ class SuitabilityCriteria:
|
|
|
41
41
|
if self.description is not None:
|
|
42
42
|
attrs.append(f"description='{self.description}'")
|
|
43
43
|
return f"{self.__class__.__name__}({', '.join(attrs) if attrs else ''})"
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
|
|
46
45
|
def compute(self) -> xr.DataArray:
|
|
47
|
-
if self.func.func_method == 'discrete':
|
|
48
|
-
sc
|
|
46
|
+
if self.func.func_method == 'discrete': # need to vectorize the discrete function
|
|
47
|
+
sc: xr.DataArray = xr.apply_ufunc(self.func.map, self.indicator).rename(self.name)
|
|
49
48
|
else:
|
|
50
|
-
sc
|
|
51
|
-
return sc.assign_attrs(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
sc: xr.DataArray = self.func.map(self.indicator).rename(self.name)
|
|
50
|
+
return sc.assign_attrs(
|
|
51
|
+
dict({k: v for k, v in self.attrs.items() if k not in ['name', 'func_method', 'from_indicator']},
|
|
52
|
+
**{
|
|
53
|
+
'history': f"func_method: {self.func}; from_indicator: [{self._from_indicator}]",
|
|
54
|
+
'compute': 'done'
|
|
55
|
+
})
|
|
56
|
+
)
|
|
57
|
+
|
|
55
58
|
@property
|
|
56
59
|
def attrs(self):
|
|
57
60
|
return {k: v for k, v in {
|
|
@@ -69,4 +72,4 @@ def _get_indicator_description(indicator: xr.Dataset | xr.DataArray) -> str:
|
|
|
69
72
|
if indicator.attrs != {}:
|
|
70
73
|
return f"name: {indicator.name}; " + "; ".join([f"{k}: {v}" for k, v in indicator.attrs.items()])
|
|
71
74
|
else:
|
|
72
|
-
return f"name: {indicator.name}"
|
|
75
|
+
return f"name: {indicator.name}"
|
lsapy/functions.py
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"""Suitability Functions definitions."""
|
|
2
2
|
|
|
3
|
-
from typing import Optional, Callable, Union, Any
|
|
4
3
|
import warnings
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
5
7
|
import matplotlib.pyplot as plt
|
|
6
8
|
import numpy as np
|
|
7
|
-
import xarray as xr
|
|
8
9
|
from scipy.optimize import curve_fit
|
|
9
10
|
|
|
10
|
-
|
|
11
11
|
__all__ = [
|
|
12
12
|
"SuitabilityFunction",
|
|
13
13
|
"MembershipSuitFunction",
|
|
@@ -19,42 +19,42 @@ class SuitabilityFunction:
|
|
|
19
19
|
|
|
20
20
|
def __init__(
|
|
21
21
|
self,
|
|
22
|
-
func:
|
|
23
|
-
func_method:
|
|
24
|
-
func_params:
|
|
22
|
+
func: Callable | None = None,
|
|
23
|
+
func_method: str | None = None,
|
|
24
|
+
func_params: dict[str, Any] = None
|
|
25
25
|
):
|
|
26
26
|
if func_params is not None:
|
|
27
27
|
if func is None and func_method is None:
|
|
28
28
|
raise ValueError("If `func_params` is provided, `func` or `func_method` must also be provided.")
|
|
29
|
+
else:
|
|
30
|
+
func_params = {}
|
|
29
31
|
|
|
30
32
|
self.func = func
|
|
31
33
|
self.func_method = func_method
|
|
32
34
|
self.func_params = func_params
|
|
33
35
|
if func is None and func_method is not None:
|
|
34
36
|
self.func = _get_function_from_name(func_method)
|
|
35
|
-
|
|
36
37
|
|
|
37
38
|
def __repr__(self):
|
|
38
|
-
return f"{self.__class__.__name__}(
|
|
39
|
-
|
|
39
|
+
return (f"{self.__class__.__name__}("
|
|
40
|
+
f"func={self.func.__name__}, "
|
|
41
|
+
f"func_method='{self.func_method}', "
|
|
42
|
+
f"func_params={self.func_params})")
|
|
40
43
|
|
|
41
44
|
def __call__(self, x):
|
|
42
45
|
if self.func is None:
|
|
43
46
|
raise ValueError("No function has been provided.")
|
|
44
47
|
return self.func(x, **self.func_params)
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
|
|
47
49
|
def map(self, x):
|
|
48
50
|
return self(x)
|
|
49
|
-
|
|
50
51
|
|
|
51
52
|
def plot(self, x) -> None:
|
|
52
53
|
plt.plot(x, self(x))
|
|
53
54
|
|
|
54
|
-
|
|
55
55
|
@property
|
|
56
56
|
def attrs(self):
|
|
57
|
-
if self.func_method
|
|
57
|
+
if self.func_method is None and self.func_params is None:
|
|
58
58
|
return {}
|
|
59
59
|
return {k: v for k, v in {
|
|
60
60
|
'func_method': self.func_method,
|
|
@@ -71,16 +71,17 @@ class MembershipSuitFunction(SuitabilityFunction):
|
|
|
71
71
|
|
|
72
72
|
def __init__(
|
|
73
73
|
self,
|
|
74
|
-
func:
|
|
75
|
-
func_method:
|
|
76
|
-
func_params:
|
|
74
|
+
func: Callable | None = None,
|
|
75
|
+
func_method: str | None = None,
|
|
76
|
+
func_params: dict[str, int | float] | None = None
|
|
77
77
|
):
|
|
78
78
|
super().__init__(func, func_method, func_params)
|
|
79
79
|
|
|
80
|
-
|
|
81
80
|
@staticmethod
|
|
82
|
-
def fit(x, y
|
|
83
|
-
|
|
81
|
+
def fit(x, y=None, methods: str | list[str] = 'all', plot: bool = False):
|
|
82
|
+
if y is None:
|
|
83
|
+
y = [0, .25, .5, .75, 1]
|
|
84
|
+
return _fit_mbs_functions(x, np.array(y), methods, plot)
|
|
84
85
|
|
|
85
86
|
|
|
86
87
|
def _prepare_for_fitting(methods: str | list[str] = 'all'):
|
|
@@ -92,16 +93,16 @@ def _prepare_for_fitting(methods: str | list[str] = 'all'):
|
|
|
92
93
|
elif isinstance(methods, list) or isinstance(methods, str):
|
|
93
94
|
if isinstance(methods, str):
|
|
94
95
|
methods = [methods]
|
|
95
|
-
|
|
96
|
+
|
|
96
97
|
_methods = []
|
|
97
98
|
for method in methods:
|
|
98
99
|
if method in _types:
|
|
99
100
|
[_methods.append(m) for m in equations[method.replace('_like', '')].keys()]
|
|
100
101
|
else:
|
|
101
|
-
try
|
|
102
|
+
try:
|
|
102
103
|
_get_function_from_name(method)
|
|
103
104
|
_methods.append(method)
|
|
104
|
-
except :
|
|
105
|
+
except Exception:
|
|
105
106
|
_skipped.append(method)
|
|
106
107
|
warnings.warn(f"`{method}` not found in equations. Skipped.", stacklevel=2)
|
|
107
108
|
methods = _methods
|
|
@@ -125,7 +126,7 @@ def _get_function_p0(method: str, x: np.ndarray) -> list[float]:
|
|
|
125
126
|
|
|
126
127
|
|
|
127
128
|
def _fit_mbs_functions(x, y, methods: str | list[str] = 'all', plot: bool = False):
|
|
128
|
-
|
|
129
|
+
|
|
129
130
|
skipped = []
|
|
130
131
|
methods, _skipped = _prepare_for_fitting(methods)
|
|
131
132
|
skipped.extend(_skipped)
|
|
@@ -133,22 +134,22 @@ def _fit_mbs_functions(x, y, methods: str | list[str] = 'all', plot: bool = Fals
|
|
|
133
134
|
if len(methods) == 0:
|
|
134
135
|
print(f"Skipped fitting for the following methods: {', '.join(skipped)}.")
|
|
135
136
|
raise ValueError("No methods to fit.")
|
|
136
|
-
else
|
|
137
|
+
else:
|
|
137
138
|
x_ = np.linspace(min(x), max(x), 100)
|
|
138
139
|
rms_errors = []
|
|
139
140
|
f_params = []
|
|
140
141
|
for method in methods:
|
|
141
|
-
try
|
|
142
|
+
try:
|
|
142
143
|
f = _get_function_from_name(method)
|
|
143
144
|
p0 = _get_function_p0(method, x)
|
|
144
145
|
popt, _ = curve_fit(f, x, y, p0=p0, maxfev=15000)
|
|
145
146
|
y_ = f(x_, *popt)
|
|
146
147
|
f_params.append(popt)
|
|
147
|
-
rmse = _rms_error(y,f(x, *popt))
|
|
148
|
+
rmse = _rms_error(y, f(x, *popt))
|
|
148
149
|
rms_errors.append(rmse)
|
|
149
150
|
if plot:
|
|
150
151
|
plt.plot(x_, y_, label=method + f' (RMSE={rmse:.2f})')
|
|
151
|
-
except:
|
|
152
|
+
except Exception:
|
|
152
153
|
skipped.append(method)
|
|
153
154
|
warnings.warn(f"Failed to fit `{method}`. Skipped.", stacklevel=2)
|
|
154
155
|
if plot:
|
|
@@ -158,7 +159,7 @@ def _fit_mbs_functions(x, y, methods: str | list[str] = 'all', plot: bool = Fals
|
|
|
158
159
|
|
|
159
160
|
if len(skipped) > 0:
|
|
160
161
|
print(f"Skipped fitting for the following methods: {', '.join(skipped)}.")
|
|
161
|
-
f_best, p_best = _get_best_fit(methods, rms_errors, f_params)
|
|
162
|
+
f_best, p_best = _get_best_fit([m for m in methods if m not in skipped], rms_errors, f_params)
|
|
162
163
|
return _get_function_from_name(f_best), p_best
|
|
163
164
|
|
|
164
165
|
|
|
@@ -170,7 +171,7 @@ class DiscreteSuitFunction(SuitabilityFunction):
|
|
|
170
171
|
|
|
171
172
|
def __init__(
|
|
172
173
|
self,
|
|
173
|
-
func_params:
|
|
174
|
+
func_params: dict[str, int | float] | None = None
|
|
174
175
|
):
|
|
175
176
|
self.func = discrete
|
|
176
177
|
self.func_method = 'discrete'
|
|
@@ -181,7 +182,7 @@ class DiscreteSuitFunction(SuitabilityFunction):
|
|
|
181
182
|
# ---------------------------- Utility functions ----------------------------- #
|
|
182
183
|
# ---------------------------------------------------------------------------- #
|
|
183
184
|
|
|
184
|
-
equations
|
|
185
|
+
equations: dict[str, dict] = {}
|
|
185
186
|
|
|
186
187
|
|
|
187
188
|
def _get_function_from_name(name: str) -> callable:
|
|
@@ -199,7 +200,7 @@ def equation(type: str):
|
|
|
199
200
|
----------
|
|
200
201
|
type : str
|
|
201
202
|
The type of equation to register.
|
|
202
|
-
|
|
203
|
+
|
|
203
204
|
Returns
|
|
204
205
|
-------
|
|
205
206
|
decorator
|
|
@@ -216,13 +217,13 @@ def equation(type: str):
|
|
|
216
217
|
|
|
217
218
|
|
|
218
219
|
@equation('discrete')
|
|
219
|
-
def discrete(x, rules: dict[str|int, int|float]) -> float:
|
|
220
|
+
def discrete(x, rules: dict[str | int, int | float]) -> float:
|
|
220
221
|
return np.vectorize(rules.get, otypes=[np.float32])(x, np.nan)
|
|
221
222
|
|
|
222
223
|
|
|
223
224
|
@equation('sigmoid')
|
|
224
225
|
def logistic(x, a, b):
|
|
225
|
-
return 1 / (1 + np.exp(-a*(x - b)))
|
|
226
|
+
return 1 / (1 + np.exp(-a * (x - b)))
|
|
226
227
|
|
|
227
228
|
|
|
228
229
|
@equation('sigmoid')
|
lsapy/lsa.py
CHANGED
|
@@ -1,29 +1,27 @@
|
|
|
1
1
|
"""Land Suitability definition."""
|
|
2
2
|
|
|
3
|
-
from typing import
|
|
4
|
-
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import geopandas as gpd
|
|
5
6
|
import numpy as np
|
|
6
7
|
import pandas as pd
|
|
7
|
-
import
|
|
8
|
+
import xarray as xr
|
|
8
9
|
from shapely.geometry import mapping
|
|
9
|
-
import rioxarray
|
|
10
|
-
import regionmask
|
|
11
10
|
|
|
12
11
|
from lsapy.criteria import SuitabilityCriteria
|
|
13
|
-
from lsapy.statistics import
|
|
12
|
+
from lsapy.statistics import spatial_statistics_summary, statistics_summary
|
|
14
13
|
|
|
15
14
|
__all__ = ["LandSuitability"]
|
|
16
15
|
|
|
17
16
|
|
|
18
17
|
class LandSuitability:
|
|
19
|
-
|
|
20
18
|
def __init__(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
self,
|
|
20
|
+
name: str,
|
|
21
|
+
criteria: dict[str, SuitabilityCriteria],
|
|
22
|
+
short_name: str | None = None,
|
|
23
|
+
long_name: str | None = None,
|
|
24
|
+
description: str | None = None,
|
|
27
25
|
) -> None:
|
|
28
26
|
self.name = name
|
|
29
27
|
self.criteria = criteria
|
|
@@ -31,48 +29,50 @@ class LandSuitability:
|
|
|
31
29
|
self.long_name = long_name
|
|
32
30
|
self.description = description
|
|
33
31
|
|
|
34
|
-
self._sort_criteria_by_weight()
|
|
32
|
+
self._sort_criteria_by_weight() # important if suitability as limited factor
|
|
35
33
|
self._criteria_name_list = [sc.name for sc in self.criteria.values()]
|
|
36
34
|
self._category_list = list(set([sc.category for sc in self.criteria.values()]))
|
|
37
35
|
self._get_params_by_category()
|
|
38
36
|
|
|
39
|
-
|
|
40
37
|
def __repr__(self) -> str:
|
|
41
|
-
if hasattr(self,
|
|
38
|
+
if hasattr(self, "data"):
|
|
42
39
|
return self.data.__repr__()
|
|
43
40
|
else:
|
|
44
41
|
attrs = []
|
|
45
42
|
for k, v in self.attrs.items():
|
|
46
43
|
if isinstance(v, str):
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
v_ = f"'{v}'"
|
|
45
|
+
else:
|
|
46
|
+
v_ = v
|
|
47
|
+
attrs.append(f"{k}={v_}")
|
|
49
48
|
return f"{self.__class__.__name__}({', '.join(attrs) if attrs else ''})"
|
|
50
|
-
|
|
51
49
|
|
|
52
50
|
@property
|
|
53
51
|
def attrs(self):
|
|
54
|
-
return {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
52
|
+
return {
|
|
53
|
+
k: v
|
|
54
|
+
for k, v in {
|
|
55
|
+
"name": self.name,
|
|
56
|
+
"criteria": self._criteria_name_list,
|
|
57
|
+
"short_name": self.short_name,
|
|
58
|
+
"long_name": self.long_name,
|
|
59
|
+
"description": self.description,
|
|
60
|
+
}.items()
|
|
61
|
+
if v is not None
|
|
62
|
+
}
|
|
62
63
|
|
|
63
64
|
# def __getitem__(self, key: str) -> SuitabilityCriteria:
|
|
64
65
|
# return self.criteria[key]
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
def compute_criteria_suitability(self, inplace: Optional[bool] = False) -> None | xr.Dataset:
|
|
67
|
+
def compute_criteria_suitability(self, inplace: bool | None = False) -> None | xr.Dataset:
|
|
68
68
|
sc_list = []
|
|
69
69
|
for _, sc in self.criteria.items():
|
|
70
|
-
print(f
|
|
71
|
-
if sc.indicator.attrs.get(
|
|
70
|
+
print(f"Computing {sc.name}...")
|
|
71
|
+
if sc.indicator.attrs.get("compute", "") == "done":
|
|
72
72
|
sc_list.append(sc.indicator.rename(sc.name))
|
|
73
73
|
else:
|
|
74
74
|
sc_list.append(sc.compute())
|
|
75
|
-
ls = xr.merge(sc_list, compat=
|
|
75
|
+
ls = xr.merge(sc_list, compat="override", combine_attrs="drop")
|
|
76
76
|
for sc in sc_list:
|
|
77
77
|
ls[sc.name].attrs = sc.attrs
|
|
78
78
|
ls.attrs = self.attrs
|
|
@@ -80,173 +80,205 @@ class LandSuitability:
|
|
|
80
80
|
self.data = ls
|
|
81
81
|
else:
|
|
82
82
|
return ls
|
|
83
|
-
|
|
84
83
|
|
|
85
84
|
def compute_category_suitability(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
85
|
+
self,
|
|
86
|
+
method: str,
|
|
87
|
+
keep_criteria: bool | None = False,
|
|
88
|
+
inplace: bool | None = False,
|
|
89
|
+
limit_var: bool | None = False,
|
|
90
90
|
) -> xr.Dataset:
|
|
91
|
-
if not hasattr(self,
|
|
91
|
+
if not hasattr(self, "data"):
|
|
92
92
|
ds = self.compute_criteria_suitability()
|
|
93
93
|
else:
|
|
94
94
|
ds = self.data
|
|
95
|
-
|
|
95
|
+
|
|
96
96
|
out = []
|
|
97
97
|
out_attrs = []
|
|
98
98
|
for category in self._category_list:
|
|
99
|
-
print(f
|
|
99
|
+
print(f"Computing {category}...")
|
|
100
100
|
sc_list = [sc for sc in self.criteria.values() if sc.category == category]
|
|
101
|
-
res = _aggregate_vars(
|
|
102
|
-
|
|
101
|
+
res = _aggregate_vars(
|
|
102
|
+
ds[self._criteria_by_category[category]],
|
|
103
|
+
method=method,
|
|
104
|
+
weights=[sc.weight for sc in sc_list],
|
|
105
|
+
limit_var=limit_var,
|
|
106
|
+
)
|
|
103
107
|
if isinstance(res, xr.Dataset):
|
|
104
|
-
res = res.rename({
|
|
108
|
+
res = res.rename({"limiting_factor": f"{category}", "limiting_var": f"{category}_var"})
|
|
105
109
|
else:
|
|
106
|
-
res = res.rename(f
|
|
107
|
-
res.attrs.update({
|
|
110
|
+
res = res.rename(f"{category}_suitability")
|
|
111
|
+
res.attrs.update({"long_name": f"{category.capitalize()} Suitability"})
|
|
108
112
|
out.append(res)
|
|
109
113
|
out_attrs.append(res.attrs)
|
|
110
|
-
out = xr.merge(out, compat=
|
|
114
|
+
out = xr.merge(out, compat="override", combine_attrs="drop")
|
|
111
115
|
if keep_criteria:
|
|
112
|
-
out = xr.merge([ds, out], compat=
|
|
113
|
-
for sc in ds.data_vars:
|
|
116
|
+
out = xr.merge([ds, out], compat="override", combine_attrs="drop")
|
|
117
|
+
for sc in ds.data_vars: # add attributes of criteria
|
|
114
118
|
out[sc].attrs = ds[sc].attrs
|
|
115
|
-
for i, category in enumerate(self._category_list):
|
|
116
|
-
out[f
|
|
117
|
-
|
|
119
|
+
for i, category in enumerate(self._category_list): # add attributes of category suitability
|
|
120
|
+
out[f"{category}_suitability"].attrs = out_attrs[i]
|
|
121
|
+
|
|
118
122
|
out.attrs.update(self.attrs)
|
|
119
123
|
if inplace:
|
|
120
124
|
self.data = out
|
|
121
125
|
else:
|
|
122
126
|
return out
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
127
|
+
|
|
128
|
+
def compute_suitability(
|
|
129
|
+
self,
|
|
130
|
+
method: str | dict[str, str] = "mean",
|
|
131
|
+
by_category: bool | None = False,
|
|
132
|
+
keep_all: bool | None = False,
|
|
133
|
+
inplace: bool | None = False,
|
|
134
|
+
limit_var: bool | None = False,
|
|
131
135
|
) -> xr.Dataset:
|
|
132
136
|
if isinstance(method, str):
|
|
133
137
|
cat_method, suit_method = method, method
|
|
134
138
|
elif isinstance(method, dict):
|
|
135
|
-
cat_method = method.get(
|
|
136
|
-
suit_method = method.get(
|
|
139
|
+
cat_method = method.get("category", "mean")
|
|
140
|
+
suit_method = method.get("overall", "mean")
|
|
137
141
|
else:
|
|
138
142
|
raise ValueError("Method must be a string or a dictionary.")
|
|
139
|
-
|
|
140
|
-
if not hasattr(self,
|
|
143
|
+
|
|
144
|
+
if not hasattr(self, "data"):
|
|
141
145
|
if by_category:
|
|
142
|
-
ds = self.compute_category_suitability(
|
|
146
|
+
ds = self.compute_category_suitability(
|
|
147
|
+
method=cat_method, keep_criteria=True, inplace=False, limit_var=limit_var
|
|
148
|
+
)
|
|
143
149
|
else:
|
|
144
150
|
ds = self.compute_criteria_suitability(inplace=False)
|
|
145
151
|
else:
|
|
146
152
|
ds = self.data
|
|
147
|
-
|
|
153
|
+
|
|
148
154
|
if by_category:
|
|
149
155
|
weights = [self.weights_by_category[category] for category in self._category_list]
|
|
150
|
-
on_vars = [f
|
|
151
|
-
else
|
|
156
|
+
on_vars = [f"{category}_suitability" for category in self._category_list]
|
|
157
|
+
else:
|
|
152
158
|
weights = [sc.weight for sc in self.criteria.values()]
|
|
153
159
|
on_vars = self._criteria_name_list
|
|
154
|
-
|
|
155
|
-
print(
|
|
156
|
-
out = _aggregate_vars(ds[on_vars], method=suit_method, weights=weights, limit_var=limit_var).rename(
|
|
157
|
-
|
|
160
|
+
|
|
161
|
+
print("Computing suitability...")
|
|
162
|
+
out = _aggregate_vars(ds[on_vars], method=suit_method, weights=weights, limit_var=limit_var).rename(
|
|
163
|
+
"suitability"
|
|
164
|
+
)
|
|
165
|
+
out.attrs.update({"long_name": "Suitability"})
|
|
158
166
|
out_attrs = out.attrs
|
|
159
167
|
|
|
160
168
|
if keep_all:
|
|
161
|
-
out = xr.merge([ds, out], compat=
|
|
162
|
-
for sc in ds.data_vars:
|
|
169
|
+
out = xr.merge([ds, out], compat="override", combine_attrs="drop")
|
|
170
|
+
for sc in ds.data_vars: # add attributes of criteria
|
|
163
171
|
out[sc].attrs = ds[sc].attrs
|
|
164
|
-
out[
|
|
165
|
-
|
|
172
|
+
out["suitability"].attrs = out_attrs
|
|
173
|
+
|
|
166
174
|
out.attrs.update(self.attrs)
|
|
167
175
|
if inplace:
|
|
168
176
|
self.data = out
|
|
169
177
|
else:
|
|
170
178
|
return out
|
|
171
|
-
|
|
172
179
|
|
|
173
180
|
def _sort_criteria_by_weight(self) -> dict[str, SuitabilityCriteria]:
|
|
174
181
|
self.criteria = dict(sorted(self.criteria.items(), key=lambda item: item[1].weight, reverse=True))
|
|
175
182
|
|
|
176
|
-
|
|
177
183
|
def _get_params_by_category(self):
|
|
178
184
|
self._get_criteria_by_category()
|
|
179
185
|
self._get_weights_by_category()
|
|
180
|
-
|
|
181
|
-
|
|
186
|
+
|
|
182
187
|
def _get_criteria_by_category(self) -> dict[str, list[str]]:
|
|
183
188
|
self._criteria_by_category = {category: [] for category in self._category_list}
|
|
184
189
|
for sc in self.criteria.values():
|
|
185
190
|
self._criteria_by_category[sc.category].append(sc.name)
|
|
186
|
-
|
|
187
191
|
|
|
188
192
|
def _get_weights_by_category(self) -> dict[str, float | int]:
|
|
189
193
|
self.weights_by_category = {category: [] for category in self._category_list}
|
|
190
194
|
for category in self._category_list:
|
|
191
|
-
self.weights_by_category[category] = sum(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
195
|
+
self.weights_by_category[category] = sum(
|
|
196
|
+
[sc.weight for sc in self.criteria.values() if sc.category == category]
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
def mask(
|
|
200
|
+
self,
|
|
201
|
+
mask: xr.DataArray | gpd.GeoDataFrame,
|
|
202
|
+
inplace: bool | None = False,
|
|
203
|
+
spatial_dims: tuple[str, str] | None = None,
|
|
204
|
+
crs: str | None = None,
|
|
205
|
+
invert: bool = False,
|
|
206
|
+
**kwargs,
|
|
207
|
+
) -> xr.DataArray | xr.Dataset:
|
|
208
|
+
if not hasattr(self, "data"):
|
|
198
209
|
raise ValueError("Suitability must be computed first.")
|
|
199
|
-
|
|
210
|
+
|
|
200
211
|
if inplace:
|
|
201
212
|
self.data = _mask_data(self.data, mask, spatial_dims=spatial_dims, crs=crs, invert=invert, **kwargs)
|
|
202
|
-
else:
|
|
213
|
+
else:
|
|
203
214
|
return _mask_data(self.data, mask, spatial_dims=spatial_dims, crs=crs, invert=invert, **kwargs)
|
|
204
|
-
|
|
205
215
|
|
|
206
216
|
def statistics(
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if not hasattr(self,
|
|
217
|
+
self,
|
|
218
|
+
on_vars: list | None = None,
|
|
219
|
+
on_dims: list | None = None,
|
|
220
|
+
on_dim_values: dict[str, Any] | None = None,
|
|
221
|
+
bins: list | np.ndarray | None = None,
|
|
222
|
+
bins_labels: list | None = None,
|
|
223
|
+
all_bins: bool | None = False,
|
|
224
|
+
cell_area: tuple[float | str, str] | None = None,
|
|
225
|
+
dropna: bool | None = False,
|
|
226
|
+
**kwargs,
|
|
227
|
+
) -> pd.DataFrame:
|
|
228
|
+
if not hasattr(self, "data"):
|
|
219
229
|
raise ValueError("Suitability must be computed first.")
|
|
220
|
-
|
|
221
|
-
return statistics_summary(
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
230
|
+
|
|
231
|
+
return statistics_summary(
|
|
232
|
+
self.data,
|
|
233
|
+
on_vars=on_vars,
|
|
234
|
+
on_dims=on_dims,
|
|
235
|
+
on_dim_values=on_dim_values,
|
|
236
|
+
bins=bins,
|
|
237
|
+
bins_labels=bins_labels,
|
|
238
|
+
all_bins=all_bins,
|
|
239
|
+
cell_area=cell_area,
|
|
240
|
+
dropna=dropna**kwargs,
|
|
241
|
+
)
|
|
225
242
|
|
|
226
243
|
def spatial_statistics(
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if not hasattr(self,
|
|
244
|
+
self,
|
|
245
|
+
areas: gpd.GeoDataFrame,
|
|
246
|
+
name: str | None = "area",
|
|
247
|
+
on_vars: list | None = None,
|
|
248
|
+
on_dims: list | None = None,
|
|
249
|
+
on_dim_values: dict[str, Any] | None = None,
|
|
250
|
+
bins: np.ndarray | None = None,
|
|
251
|
+
all_bins: bool | None = False,
|
|
252
|
+
cell_area: tuple[float | str, str] | None = None,
|
|
253
|
+
mask_kwargs: dict = None,
|
|
254
|
+
stats_kwargs: dict = None,
|
|
255
|
+
) -> pd.DataFrame:
|
|
256
|
+
if not hasattr(self, "data"):
|
|
240
257
|
raise ValueError("Suitability must be computed first.")
|
|
241
|
-
|
|
242
|
-
return spatial_statistics_summary(self.data, areas, name=name, on_vars=on_vars, on_dims=on_dims, on_dim_values=on_dim_values,
|
|
243
|
-
bins=bins, all_bins=all_bins, cell_area=cell_area, mask_kwargs=mask_kwargs, stats_kwargs=stats_kwargs)
|
|
244
|
-
|
|
245
258
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
259
|
+
return spatial_statistics_summary(
|
|
260
|
+
self.data,
|
|
261
|
+
areas,
|
|
262
|
+
name=name,
|
|
263
|
+
on_vars=on_vars,
|
|
264
|
+
on_dims=on_dims,
|
|
265
|
+
on_dim_values=on_dim_values,
|
|
266
|
+
bins=bins,
|
|
267
|
+
all_bins=all_bins,
|
|
268
|
+
cell_area=cell_area,
|
|
269
|
+
mask_kwargs=mask_kwargs,
|
|
270
|
+
stats_kwargs=stats_kwargs,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _mask_data(
|
|
275
|
+
data: xr.DataArray | xr.Dataset,
|
|
276
|
+
mask: xr.DataArray | gpd.GeoDataFrame,
|
|
277
|
+
spatial_dims: tuple[str, str] | None = None,
|
|
278
|
+
crs: str | None = None,
|
|
279
|
+
invert: bool = False,
|
|
280
|
+
**kwargs,
|
|
281
|
+
) -> xr.DataArray | xr.Dataset:
|
|
250
282
|
if isinstance(mask, gpd.GeoDataFrame):
|
|
251
283
|
mask = mask.to_crs(crs)
|
|
252
284
|
data = data.rio.set_spatial_dims(*spatial_dims).rio.write_crs(crs)
|
|
@@ -256,76 +288,102 @@ def _mask_data(data : xr.DataArray | xr.Dataset, mask : Union[xr.DataArray, gpd.
|
|
|
256
288
|
mask = ~mask
|
|
257
289
|
return data.where(mask, **kwargs)
|
|
258
290
|
else:
|
|
259
|
-
raise ValueError(
|
|
291
|
+
raise ValueError("mask must be a GeoDataFrame or DataArray")
|
|
260
292
|
|
|
261
293
|
|
|
262
294
|
####################################################################################################
|
|
263
295
|
# VARIABLES AGGREGATION FUNCTIONS
|
|
264
296
|
####################################################################################################
|
|
265
297
|
|
|
266
|
-
|
|
298
|
+
|
|
299
|
+
def vars_weighted_mean(ds: xr.Dataset, vars=None, weights=None) -> xr.DataArray:
|
|
267
300
|
if vars is None:
|
|
268
301
|
vars = list(ds.data_vars)
|
|
269
302
|
if weights is None:
|
|
270
303
|
weights = np.ones(len(vars))
|
|
271
|
-
|
|
272
|
-
s = sum([ds[v] * w for v, w in zip(vars, weights)])
|
|
273
|
-
da: xr.DataArray = s / sum(weights)
|
|
274
|
-
return da.assign_attrs({
|
|
275
|
-
'method': 'Weighted Mean',
|
|
276
|
-
'description': f"Weighted Mean of variables: {', '.join([f'{v} ({w})' for v, w in zip(vars, weights)])}."
|
|
277
|
-
}).rename('weighted_mean')
|
|
278
304
|
|
|
279
|
-
|
|
280
|
-
|
|
305
|
+
s = sum([ds[v] * w for v, w in zip(vars, weights, strict=False)])
|
|
306
|
+
da: xr.DataArray = s / sum(weights)
|
|
307
|
+
return da.assign_attrs(
|
|
308
|
+
{
|
|
309
|
+
"method": "Weighted Mean",
|
|
310
|
+
"descritpion": (
|
|
311
|
+
"Weighted Mean of variables: "
|
|
312
|
+
f"{', '.join([f'{v} ({w})' for v, w in zip(vars, weights, strict=False)])}."
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
).rename("weighted_mean")
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def vars_mean(ds: xr.Dataset, vars=None) -> xr.DataArray:
|
|
319
|
+
if vars is None:
|
|
320
|
+
vars = list(ds.data_vars)
|
|
281
321
|
da = vars_weighted_mean(ds, vars=vars)
|
|
282
|
-
return da.assign_attrs({
|
|
322
|
+
return da.assign_attrs({"method": "Mean", "description": f"Mean of variables: {', '.join(vars)}."}).rename("mean")
|
|
323
|
+
|
|
283
324
|
|
|
284
|
-
def vars_weighted_geomean(ds: xr.Dataset, vars
|
|
325
|
+
def vars_weighted_geomean(ds: xr.Dataset, vars=None, weights=None) -> xr.DataArray:
|
|
285
326
|
if vars is None:
|
|
286
327
|
vars = list(ds.data_vars)
|
|
287
328
|
if weights is None:
|
|
288
329
|
weights = np.ones(len(vars))
|
|
289
330
|
|
|
290
|
-
s = sum([np.log(ds[v]) * w for v, w in zip(vars, weights)])
|
|
291
|
-
da
|
|
292
|
-
return da.assign_attrs(
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
331
|
+
s = sum([np.log(ds[v]) * w for v, w in zip(vars, weights, strict=False)])
|
|
332
|
+
da: xr.DataArray = np.exp(s / sum(weights))
|
|
333
|
+
return da.assign_attrs(
|
|
334
|
+
{
|
|
335
|
+
"method": "Weighted Geometric Mean",
|
|
336
|
+
"description": (
|
|
337
|
+
"Weighted Geometric Mean of variables: "
|
|
338
|
+
f"{', '.join([f'{v} ({w})' for v, w in zip(vars, weights, strict=False)])}."
|
|
339
|
+
)
|
|
340
|
+
}
|
|
341
|
+
).rename("weighted_geometric_mean")
|
|
296
342
|
|
|
297
|
-
|
|
298
|
-
|
|
343
|
+
|
|
344
|
+
def vars_geomean(ds: xr.Dataset, vars=None) -> xr.DataArray:
|
|
345
|
+
if vars is None:
|
|
346
|
+
vars = list(ds.data_vars)
|
|
299
347
|
da = vars_weighted_geomean(ds, vars=vars)
|
|
300
|
-
return da.assign_attrs(
|
|
348
|
+
return da.assign_attrs(
|
|
349
|
+
{"method": "Geometric Mean", "description": f"Geometric Mean of variables: {', '.join(vars)}."}
|
|
350
|
+
).rename("geometric_mean")
|
|
351
|
+
|
|
301
352
|
|
|
302
|
-
def limiting_factor(ds: xr.Dataset, vars
|
|
353
|
+
def limiting_factor(ds: xr.Dataset, vars=None, limiting_var: bool | None = True) -> xr.DataArray | xr.Dataset:
|
|
303
354
|
if vars is None:
|
|
304
355
|
vars = list(ds.data_vars)
|
|
305
|
-
|
|
356
|
+
|
|
306
357
|
da = ds[vars].to_array()
|
|
307
|
-
mask = da.notnull().all(dim=
|
|
358
|
+
mask = da.notnull().all(dim="variable")
|
|
308
359
|
|
|
309
|
-
lim = da.min(dim=
|
|
310
|
-
lim = lim.assign_attrs(
|
|
360
|
+
lim = da.min(dim="variable", skipna=True).where(mask).rename("limiting_factor")
|
|
361
|
+
lim = lim.assign_attrs(
|
|
362
|
+
{"method": "Limiting Factor", "description": f"Value of limiting factor among variables: {', '.join(vars)}."}
|
|
363
|
+
)
|
|
311
364
|
if limiting_var:
|
|
312
|
-
lim_var = da.fillna(9999).argmin(dim=
|
|
313
|
-
lim_var.attrs = {
|
|
314
|
-
|
|
315
|
-
|
|
365
|
+
lim_var = da.fillna(9999).argmin(dim="variable", skipna=True).where(mask).rename("limiting_var")
|
|
366
|
+
lim_var.attrs = {
|
|
367
|
+
"method": "Limiting Factor",
|
|
368
|
+
"description": f"Limiting factor among: {', '.join(vars)}.",
|
|
369
|
+
"legend": {f"{i}": v for i, v in enumerate(vars)},
|
|
370
|
+
}
|
|
316
371
|
return xr.merge([lim, lim_var])
|
|
317
372
|
return lim.to_dataset()
|
|
318
373
|
|
|
319
|
-
|
|
320
|
-
|
|
374
|
+
|
|
375
|
+
def _aggregate_vars(
|
|
376
|
+
ds: xr.Dataset, method: str = "mean", vars=None, weights=None, **kwargs
|
|
377
|
+
) -> xr.DataArray | xr.Dataset:
|
|
378
|
+
if method.lower() == "mean":
|
|
321
379
|
return vars_mean(ds, vars=vars)
|
|
322
|
-
elif method.lower() ==
|
|
380
|
+
elif method.lower() == "weighted_mean":
|
|
323
381
|
return vars_weighted_mean(ds, vars=vars, weights=weights)
|
|
324
|
-
elif method.lower() ==
|
|
382
|
+
elif method.lower() == "geomean":
|
|
325
383
|
return vars_geomean(ds, vars=vars)
|
|
326
|
-
elif method.lower() ==
|
|
384
|
+
elif method.lower() == "weighted_geomean":
|
|
327
385
|
return vars_weighted_geomean(ds, vars=vars, weights=weights)
|
|
328
|
-
elif method.lower() ==
|
|
386
|
+
elif method.lower() == "limit_factor":
|
|
329
387
|
return limiting_factor(ds, vars=vars, **kwargs)
|
|
330
388
|
else:
|
|
331
389
|
raise ValueError(f"Method '{method}' not recognized.")
|
lsapy/statistics.py
CHANGED
|
@@ -1,67 +1,70 @@
|
|
|
1
|
-
|
|
1
|
+
"""Statistics Module"""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import geopandas as gpd
|
|
2
6
|
import numpy as np
|
|
3
7
|
import pandas as pd
|
|
4
|
-
import geopandas as gpd
|
|
5
|
-
import xarray as xr
|
|
6
8
|
import regionmask
|
|
9
|
+
import xarray as xr
|
|
7
10
|
|
|
8
11
|
|
|
9
12
|
def statistics_summary(
|
|
10
13
|
data: xr.DataArray | xr.Dataset,
|
|
11
|
-
on_vars:
|
|
12
|
-
on_dims:
|
|
13
|
-
on_dim_values:
|
|
14
|
-
bins
|
|
15
|
-
bins_labels
|
|
16
|
-
all_bins
|
|
17
|
-
cell_area
|
|
18
|
-
dropna:
|
|
14
|
+
on_vars: list | None = None,
|
|
15
|
+
on_dims: list | None = None,
|
|
16
|
+
on_dim_values: dict[str, Any] | None = None,
|
|
17
|
+
bins: list | np.ndarray | None = None,
|
|
18
|
+
bins_labels: list | None = None,
|
|
19
|
+
all_bins: bool | None = False,
|
|
20
|
+
cell_area: tuple[float | str, str] | None = None,
|
|
21
|
+
dropna: bool | None = False,
|
|
19
22
|
**kwargs) -> pd.DataFrame:
|
|
20
|
-
|
|
23
|
+
|
|
21
24
|
def _correct_lowest_cut_interal(x: pd.Series) -> pd.Series:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if bins is not None and bins_labels is not None and (len(bins)-1 != len(bins_labels)):
|
|
25
|
+
first_cat = x.cat.categories[0]
|
|
26
|
+
lf = first_cat.left + (abs(first_cat.left) - bins[0])
|
|
27
|
+
return x.cat.rename_categories({first_cat: pd.Interval(lf, first_cat.right, closed='both')})
|
|
28
|
+
|
|
29
|
+
if bins is not None and bins_labels is not None and (len(bins) - 1 != len(bins_labels)):
|
|
28
30
|
raise ValueError('bins and bins_labels must have the same length')
|
|
29
|
-
|
|
31
|
+
|
|
30
32
|
if on_dim_values is not None:
|
|
31
33
|
for dim, value in on_dim_values.items():
|
|
32
34
|
data = data.sel({dim: value})
|
|
33
|
-
|
|
35
|
+
|
|
34
36
|
if on_vars is None:
|
|
35
37
|
on_vars = list(data.data_vars)
|
|
36
38
|
data = data[on_vars]
|
|
37
39
|
if on_dims is None:
|
|
38
40
|
on_dims = list(data.dims)
|
|
39
|
-
on_dims = [d for d in on_dims if d not in ['lat', 'lon', 'x', 'y']]
|
|
41
|
+
on_dims = [d for d in on_dims if d not in ['lat', 'lon', 'x', 'y']] # remove spatial dims
|
|
40
42
|
if cell_area:
|
|
41
43
|
cell_area, cell_unit = cell_area
|
|
42
44
|
|
|
43
|
-
|
|
44
45
|
df = data.to_dataframe().reset_index()
|
|
45
46
|
if len(on_dims) > 0:
|
|
46
47
|
df = df.drop(columns=[c for c in data.coords if c not in on_dims])
|
|
47
|
-
|
|
48
|
+
|
|
48
49
|
df = df.melt(id_vars=on_dims)
|
|
49
50
|
_dims = ['variable'] + on_dims
|
|
50
51
|
|
|
51
52
|
if bins is not None:
|
|
52
53
|
df['bin'] = pd.cut(df['value'], bins=pd.Index(bins), **kwargs)
|
|
53
|
-
if 'include_lowest' in kwargs and kwargs['include_lowest']
|
|
54
|
+
if 'include_lowest' in kwargs and kwargs['include_lowest']:
|
|
54
55
|
df['bin'] = _correct_lowest_cut_interal(df['bin'])
|
|
55
56
|
|
|
56
57
|
if bins_labels is not None:
|
|
57
|
-
lab_mapping = dict(zip(df['bin'].cat.categories.astype(str), bins_labels))
|
|
58
|
+
lab_mapping = dict(zip(df['bin'].cat.categories.astype(str), bins_labels, strict=False))
|
|
58
59
|
_dims.append('bin')
|
|
59
60
|
if all_bins:
|
|
60
|
-
all_bins_inter = pd.Interval(
|
|
61
|
+
all_bins_inter = pd.Interval(
|
|
62
|
+
df['bin'].cat.categories[0].left, df['bin'].cat.categories[-1].right, closed='both'
|
|
63
|
+
)
|
|
61
64
|
df_ = df.drop(columns=['bin']).assign(bin=all_bins_inter)
|
|
62
65
|
df_.loc[df['value'].isnull(), 'bin'] = np.nan
|
|
63
66
|
if bins_labels is not None:
|
|
64
|
-
lab_mapping.update({str(all_bins_inter)
|
|
67
|
+
lab_mapping.update({str(all_bins_inter): 'all'})
|
|
65
68
|
df = pd.concat([df, df_])
|
|
66
69
|
df['bin'] = df['bin'].astype(str)
|
|
67
70
|
|
|
@@ -73,7 +76,7 @@ def statistics_summary(
|
|
|
73
76
|
|
|
74
77
|
if cell_area:
|
|
75
78
|
df_out[f'area_{cell_unit}'] = df_out['count'] * cell_area
|
|
76
|
-
|
|
79
|
+
|
|
77
80
|
if dropna:
|
|
78
81
|
return df_out.dropna()
|
|
79
82
|
return df_out
|
|
@@ -81,19 +84,24 @@ def statistics_summary(
|
|
|
81
84
|
|
|
82
85
|
def spatial_statistics_summary(
|
|
83
86
|
data: xr.DataArray | xr.Dataset,
|
|
84
|
-
areas
|
|
85
|
-
name
|
|
86
|
-
on_vars:
|
|
87
|
-
on_dims:
|
|
88
|
-
on_dim_values:
|
|
89
|
-
bins
|
|
90
|
-
bins_labels
|
|
91
|
-
all_bins
|
|
92
|
-
cell_area
|
|
93
|
-
dropna:
|
|
94
|
-
mask_kwargs
|
|
95
|
-
stats_kwargs: dict =
|
|
96
|
-
|
|
87
|
+
areas: gpd.GeoDataFrame,
|
|
88
|
+
name: str | None = 'area',
|
|
89
|
+
on_vars: list | None = None,
|
|
90
|
+
on_dims: list | None = None,
|
|
91
|
+
on_dim_values: dict[str, Any] | None = None,
|
|
92
|
+
bins: np.ndarray | None = None,
|
|
93
|
+
bins_labels: list | None = None,
|
|
94
|
+
all_bins: bool | None = False,
|
|
95
|
+
cell_area: tuple[float | str, str] | None = None,
|
|
96
|
+
dropna: bool | None = False,
|
|
97
|
+
mask_kwargs: dict = None,
|
|
98
|
+
stats_kwargs: dict = None) -> pd.DataFrame:
|
|
99
|
+
|
|
100
|
+
if mask_kwargs is None:
|
|
101
|
+
mask_kwargs = {}
|
|
102
|
+
if stats_kwargs is None:
|
|
103
|
+
stats_kwargs = {}
|
|
104
|
+
|
|
97
105
|
regions = regionmask.from_geopandas(areas, name=name, **mask_kwargs)
|
|
98
106
|
mask = regions.mask_3D(data)
|
|
99
107
|
|
|
@@ -102,8 +110,8 @@ def spatial_statistics_summary(
|
|
|
102
110
|
df = statistics_summary(
|
|
103
111
|
data.where(mask.sel(region=r)), on_vars=on_vars, on_dims=on_dims, on_dim_values=on_dim_values,
|
|
104
112
|
bins=bins, bins_labels=bins_labels, all_bins=all_bins,
|
|
105
|
-
cell_area=cell_area
|
|
113
|
+
cell_area=cell_area, dropna=dropna, **stats_kwargs
|
|
106
114
|
)
|
|
107
115
|
df.insert(0, name, regions[r].name)
|
|
108
116
|
out.append(df)
|
|
109
|
-
return pd.concat(out)
|
|
117
|
+
return pd.concat(out)
|
lsapy/utils.py
CHANGED
|
@@ -1,28 +1,24 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Miscellaneous Utilities
|
|
3
|
-
===============================
|
|
4
|
-
|
|
5
|
-
...
|
|
6
|
-
"""
|
|
1
|
+
"""Miscellaneous Utilities"""
|
|
7
2
|
|
|
8
3
|
from pathlib import Path
|
|
9
4
|
|
|
10
5
|
import xarray as xr
|
|
11
6
|
|
|
7
|
+
|
|
12
8
|
def _load_data(name: str) -> xr.Dataset:
|
|
13
9
|
"""Load a sample dataset for testing or example purposes."""
|
|
14
|
-
if name.lower() not in [
|
|
10
|
+
if name.lower() not in ["soil", "climate"]:
|
|
15
11
|
raise ValueError(f"Invalid data name: {name}. Must be one of 'soil' or 'climate'.")
|
|
16
|
-
|
|
17
|
-
data_path = Path(__file__).parent /
|
|
18
|
-
return xr.open_dataset(data_path / f
|
|
12
|
+
|
|
13
|
+
data_path = Path(__file__).parent / "data"
|
|
14
|
+
return xr.open_dataset(data_path / f"{name.lower()}_sample.nc")
|
|
19
15
|
|
|
20
16
|
|
|
21
17
|
def load_soil_data() -> xr.Dataset:
|
|
22
18
|
"""Load soil dataset for testing or example purposes."""
|
|
23
|
-
return _load_data(
|
|
19
|
+
return _load_data("soil")
|
|
24
20
|
|
|
25
21
|
|
|
26
22
|
def load_climate_data() -> xr.Dataset:
|
|
27
23
|
"""Load climate dataset for testing or example purposes."""
|
|
28
|
-
return _load_data(
|
|
24
|
+
return _load_data("climate")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lsapy
|
|
3
|
-
Version: 0.1.0.
|
|
3
|
+
Version: 0.1.0.dev1
|
|
4
4
|
Summary: A Python package for Land Suitability Analysis.
|
|
5
5
|
Keywords: LSAPy,Land Suitability Analysis,Spatial Analysis,GIS,Geography
|
|
6
6
|
Author-email: Baptiste Hamon <baptiste.hamon@pg.canterbury.ac.nz>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
lsapy/__init__.py,sha256=SreXtKw-kHRCBJB4s2JLW4IeJlgQzwpzKMZH4hcdzA8,286
|
|
2
|
+
lsapy/criteria.py,sha256=7VFl1YQcZ015KELsLPsRFFEvGyvaj07Vkj79sM4t7p0,2952
|
|
3
|
+
lsapy/functions.py,sha256=VaQS4ONrKWN5iAXT_xK_Ess1QXvodGsWzlBk8q-PO88,8513
|
|
4
|
+
lsapy/lsa.py,sha256=AewMwvNzYdL3yGBAuncxy35G4oDybMaHqWGfWCxECNo,14459
|
|
5
|
+
lsapy/statistics.py,sha256=lovmy94-km12ObcDj4TlrVpyZDLCvJTqyLHIgRWF0mw,4313
|
|
6
|
+
lsapy/utils.py,sha256=y8-WL2wrPPIHeCwrqXERyY-X71qXnMAosUAmyUGjqU4,732
|
|
7
|
+
lsapy/data/climate_sample.nc,sha256=ng2o7BxKLgzQWIl5fdiGWvdCQSeHeOO5-X3ciyD72Xo,52687793
|
|
8
|
+
lsapy/data/soil_sample.nc,sha256=y7NpYV9VZKXwiYExynA0vF1KAn5rhsWqzwGJ4tFE-TQ,2535031
|
|
9
|
+
lsapy-0.1.0.dev1.dist-info/licenses/LICENSE,sha256=6kbiFSfobTZ7beWiKnHpN902HgBx-Jzgcme0SvKqhKY,1091
|
|
10
|
+
lsapy-0.1.0.dev1.dist-info/WHEEL,sha256=_2ozNFCLWc93bK4WKHCO-eDUENDlo-dgc9cU3qokYO4,82
|
|
11
|
+
lsapy-0.1.0.dev1.dist-info/METADATA,sha256=kyssK7TWoRG-yNKHLbfW80y1wXmglG8emgF_KkV-dgQ,1145
|
|
12
|
+
lsapy-0.1.0.dev1.dist-info/RECORD,,
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
lsapy/__init__.py,sha256=MXu8EIXhOrPg7Aba-9nrQCU51QTRAWNKxDapzYcnzFs,284
|
|
2
|
-
lsapy/criteria.py,sha256=f2piZaEecKsLBvIH4JhLNXDNfNKNXUp70r23oeHErcQ,2971
|
|
3
|
-
lsapy/functions.py,sha256=WYfpUREOsrkv-70wme_zL_HArM_AI2b5YxU8vtee5NI,8382
|
|
4
|
-
lsapy/lsa.py,sha256=sBx96OpTDeaSrcX9s8IB2k91CNl0WUIW9hVM6oP-Hqg,14339
|
|
5
|
-
lsapy/statistics.py,sha256=62O57pqF1Pth4s1D0MbQEyobnxDGZ2bNatC2uVFnHH8,4269
|
|
6
|
-
lsapy/utils.py,sha256=eezLr5l0gqqgFEglzsSnbwx7JqOlVeKK3G63qllAJrg,776
|
|
7
|
-
lsapy/data/climate_sample.nc,sha256=ng2o7BxKLgzQWIl5fdiGWvdCQSeHeOO5-X3ciyD72Xo,52687793
|
|
8
|
-
lsapy/data/soil_sample.nc,sha256=y7NpYV9VZKXwiYExynA0vF1KAn5rhsWqzwGJ4tFE-TQ,2535031
|
|
9
|
-
lsapy-0.1.0.dev0.dist-info/licenses/LICENSE,sha256=6kbiFSfobTZ7beWiKnHpN902HgBx-Jzgcme0SvKqhKY,1091
|
|
10
|
-
lsapy-0.1.0.dev0.dist-info/WHEEL,sha256=_2ozNFCLWc93bK4WKHCO-eDUENDlo-dgc9cU3qokYO4,82
|
|
11
|
-
lsapy-0.1.0.dev0.dist-info/METADATA,sha256=Vr0peYa--97JnnQKHtAsNb8SdreyYCs7espEr4XSemg,1145
|
|
12
|
-
lsapy-0.1.0.dev0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|