wolfhece 2.2.30__py3-none-any.whl → 2.2.32__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.
- wolfhece/PyVertexvectors.py +22 -0
- wolfhece/analyze_poly.py +1189 -24
- wolfhece/apps/version.py +1 -1
- wolfhece/pydownloader.py +182 -0
- wolfhece/pypolygons_scen.py +7 -9
- wolfhece/wolf_array.py +147 -64
- {wolfhece-2.2.30.dist-info → wolfhece-2.2.32.dist-info}/METADATA +1 -1
- {wolfhece-2.2.30.dist-info → wolfhece-2.2.32.dist-info}/RECORD +11 -10
- {wolfhece-2.2.30.dist-info → wolfhece-2.2.32.dist-info}/WHEEL +0 -0
- {wolfhece-2.2.30.dist-info → wolfhece-2.2.32.dist-info}/entry_points.txt +0 -0
- {wolfhece-2.2.30.dist-info → wolfhece-2.2.32.dist-info}/top_level.txt +0 -0
wolfhece/analyze_poly.py
CHANGED
@@ -4,10 +4,11 @@ import numpy as np
|
|
4
4
|
from shapely.geometry import Point, LineString
|
5
5
|
from typing import Literal
|
6
6
|
import pandas as pd
|
7
|
+
from pathlib import Path
|
7
8
|
|
8
9
|
from .PyTranslate import _
|
9
10
|
from .drawing_obj import Element_To_Draw
|
10
|
-
from .PyVertexvectors import Triangulation, vector,Zones, zone
|
11
|
+
from .PyVertexvectors import Triangulation, vector,Zones, zone, Polygon
|
11
12
|
from .wolf_array import WolfArray, header_wolf
|
12
13
|
|
13
14
|
class Array_analysis_onepolygon():
|
@@ -20,63 +21,119 @@ class Array_analysis_onepolygon():
|
|
20
21
|
Plots of the values distribution can be generated using seaborn or plotly.
|
21
22
|
"""
|
22
23
|
|
23
|
-
def __init__(self, wa:WolfArray, polygon:vector):
|
24
|
+
def __init__(self, wa:WolfArray, polygon:vector, buffer_size:float = 0.0):
|
24
25
|
|
25
26
|
self._wa = wa
|
26
|
-
|
27
|
+
|
28
|
+
if buffer_size > 0.0:
|
29
|
+
self._polygon = polygon.buffer(buffer_size, inplace=False)
|
30
|
+
elif buffer_size == 0.0:
|
31
|
+
self._polygon = polygon
|
32
|
+
else:
|
33
|
+
raise ValueError("Buffer size must be greater than or equal to 0.0.")
|
27
34
|
|
28
35
|
self._selected_cells = None
|
29
36
|
self._values = None
|
30
37
|
|
31
|
-
|
38
|
+
@ property
|
39
|
+
def centroid(self) -> Point:
|
40
|
+
""" Get the centroid of the polygon as a Point object.
|
41
|
+
|
42
|
+
:return: Shapely Point object representing the centroid of the polygon
|
43
|
+
"""
|
44
|
+
return self._polygon.centroid
|
45
|
+
|
46
|
+
def values(self, which:Literal['Mean', 'Std', 'Median', 'Sum', 'Volume', 'Values', 'Area']) -> pd.DataFrame | float:
|
32
47
|
""" Get the values as a pandas DataFrame
|
33
48
|
|
34
49
|
:param which: Mean, Std, Median, Sum, Volume, Values
|
35
50
|
"""
|
36
51
|
|
37
|
-
|
38
|
-
if which not in
|
39
|
-
raise ValueError(f"Invalid value for 'which'. Must be one of {
|
52
|
+
authorized = ['Mean', 'Std', 'Median', 'Sum', 'Volume', 'Values', 'Area']
|
53
|
+
if which not in authorized:
|
54
|
+
raise ValueError(f"Invalid value for 'which'. Must be one of {authorized}.")
|
40
55
|
|
41
56
|
if self._values is None:
|
42
57
|
self.compute_values()
|
43
58
|
|
59
|
+
if which == 'Area' and 'Area' not in self._values:
|
60
|
+
self._add_area2velues()
|
61
|
+
|
44
62
|
if self._values is None:
|
45
63
|
raise ValueError("No values computed. Please call compute_values() first.")
|
46
64
|
|
47
65
|
if which == 'Values':
|
48
|
-
return pd.DataFrame(self._values[_(which)], columns=[
|
66
|
+
return pd.DataFrame(self._values[_(which)], columns=[self._polygon.myname])
|
49
67
|
else:
|
50
68
|
return self._values[which]
|
51
69
|
|
70
|
+
def as_vector(self, add_values:bool = True):
|
71
|
+
""" Return a copy of the polygon with the values as attributes. """
|
72
|
+
|
73
|
+
newvec = self._polygon.deepcopy()
|
74
|
+
|
75
|
+
if add_values:
|
76
|
+
if self._values is None:
|
77
|
+
self.compute_values()
|
78
|
+
self._add_area2velues()
|
79
|
+
|
80
|
+
if self._values is None:
|
81
|
+
raise ValueError("No values computed. Please call compute_values() first.")
|
82
|
+
|
83
|
+
for key, value in self._values.items():
|
84
|
+
newvec.add_value(key, value)
|
85
|
+
|
86
|
+
newvec.myname = self._polygon.myname
|
87
|
+
return newvec
|
88
|
+
|
52
89
|
def select_cells(self, mode:Literal['polygon', 'buffer'] = 'polygon', **kwargs):
|
53
|
-
"""
|
90
|
+
"""Select the cells inside the polygon.
|
91
|
+
|
92
|
+
:param mode: 'polygon' or 'buffer'
|
93
|
+
:param kwargs: 'polygon' for polygon selection or 'buffer' for buffer size
|
94
|
+
|
95
|
+
For polygon selection, the polygon must be provided in kwargs or use the polygon set during initialization.
|
96
|
+
For buffer selection, the buffer size in meter must be provided in kwargs.
|
97
|
+
"""
|
54
98
|
|
55
99
|
if mode == 'polygon':
|
56
100
|
if 'polygon' in kwargs:
|
57
101
|
self._polygon = kwargs['polygon']
|
58
|
-
|
59
|
-
else:
|
102
|
+
elif self._polygon is None:
|
60
103
|
raise ValueError("No polygon provided. Please provide a polygon to select cells.")
|
104
|
+
self._select_cells_polygon(self._polygon)
|
105
|
+
|
61
106
|
elif mode == 'buffer':
|
62
|
-
if 'buffer' in kwargs:
|
107
|
+
if 'buffer' in kwargs and self._polygon is not None:
|
63
108
|
self._select_cells_buffer(kwargs['buffer'])
|
64
109
|
else:
|
65
110
|
raise ValueError("No buffer size provided. Please provide a buffer size to select cells.")
|
66
111
|
else:
|
67
112
|
raise ValueError("Invalid mode. Please use 'polygon' or 'buffer'.")
|
68
113
|
|
69
|
-
def _select_cells_polygon(self, selection_poly:vector):
|
114
|
+
def _select_cells_polygon(self, selection_poly:vector = None):
|
70
115
|
""" Select the cells inside the polygon """
|
71
116
|
|
72
|
-
|
73
|
-
|
117
|
+
if selection_poly is None:
|
118
|
+
if self._polygon is None:
|
119
|
+
raise ValueError("No polygon provided. Please provide a polygon to select cells.")
|
120
|
+
selection_poly = self._polygon
|
121
|
+
else:
|
122
|
+
self._polygon = selection_poly
|
123
|
+
|
124
|
+
self._selected_cells = self._wa.get_xy_inside_polygon(selection_poly)
|
74
125
|
|
75
126
|
def _select_cells_buffer(self, buffer_size:float = 0.0):
|
76
127
|
""" Select the cells inside the buffer of the polygon """
|
77
128
|
|
78
|
-
|
79
|
-
|
129
|
+
if buffer_size > 0.0:
|
130
|
+
selection_poly = self._polygon.buffer(buffer_size, inplace=False)
|
131
|
+
elif buffer_size == 0.0:
|
132
|
+
selection_poly = self._polygon
|
133
|
+
else:
|
134
|
+
raise ValueError("Buffer size must be greater than or equal to 0.0.")
|
135
|
+
|
136
|
+
self._selected_cells = self._wa.get_xy_inside_polygon(selection_poly)
|
80
137
|
|
81
138
|
def compute_values(self):
|
82
139
|
""" Get the values of the array inside the polygon """
|
@@ -85,7 +142,48 @@ class Array_analysis_onepolygon():
|
|
85
142
|
if self._polygon is None:
|
86
143
|
raise ValueError("No polygon provided. Please provide a polygon to select cells.")
|
87
144
|
|
88
|
-
|
145
|
+
self._values = self._wa.statistics(self._polygon)
|
146
|
+
else:
|
147
|
+
self._values = self._wa.statistics(self._selected_cells)
|
148
|
+
|
149
|
+
def _add_area2velues(self):
|
150
|
+
""" Add the area of the polygon to the values """
|
151
|
+
|
152
|
+
if self._selected_cells is None:
|
153
|
+
if self._polygon is None:
|
154
|
+
raise ValueError("No polygon provided. Please provide a polygon to select cells.")
|
155
|
+
|
156
|
+
self._values['Area'] = self._polygon.area
|
157
|
+
centroid = self._polygon.centroid
|
158
|
+
self._values['X'] = centroid.x
|
159
|
+
self._values['Y'] = centroid.y
|
160
|
+
else:
|
161
|
+
self._values['Area'] = len(self._selected_cells) * self._wa.dx * self._wa.dy
|
162
|
+
self._values['X'] = np.mean(self._selected_cells[:, 0])
|
163
|
+
self._values['Y'] = np.mean(self._selected_cells[:, 1])
|
164
|
+
|
165
|
+
@property
|
166
|
+
def n_selected_cells(self) -> int:
|
167
|
+
""" Get the number of selected cells """
|
168
|
+
if self._selected_cells is None:
|
169
|
+
return 0
|
170
|
+
|
171
|
+
return len(self._selected_cells)
|
172
|
+
|
173
|
+
def get_selection(self) -> np.ndarray:
|
174
|
+
""" Get the selected cells as a numpy array of coordinates.
|
175
|
+
|
176
|
+
:return: numpy array of shape (n, 2) with the coordinates of the selected cells
|
177
|
+
"""
|
178
|
+
if self._selected_cells is None:
|
179
|
+
raise ValueError("No cells selected. Please call select_cells() first.")
|
180
|
+
|
181
|
+
return np.array(self._selected_cells)
|
182
|
+
|
183
|
+
def reset_selection(self):
|
184
|
+
""" Reset the selection of cells """
|
185
|
+
self._selected_cells = None
|
186
|
+
self._values = None
|
89
187
|
|
90
188
|
def plot_values(self, show:bool = True, bins:int = 100,
|
91
189
|
engine:Literal['seaborn', 'plotly'] = 'seaborn'):
|
@@ -153,6 +251,41 @@ class Array_analysis_onepolygon():
|
|
153
251
|
|
154
252
|
return fig
|
155
253
|
|
254
|
+
@property
|
255
|
+
def has_values(self) -> bool:
|
256
|
+
""" Check if there useful values """
|
257
|
+
if self._values is None:
|
258
|
+
self.compute_values()
|
259
|
+
|
260
|
+
return len(self.values('Values')) > 0
|
261
|
+
|
262
|
+
@property
|
263
|
+
def has_strictly_positive_values(self) -> bool:
|
264
|
+
""" Check if there useful values """
|
265
|
+
if self._values is None:
|
266
|
+
self.compute_values()
|
267
|
+
|
268
|
+
return len(self.values('Values') > 0) > 0
|
269
|
+
|
270
|
+
def distribute_values(self, bins:list[float]):
|
271
|
+
""" Distribute the values in bins
|
272
|
+
|
273
|
+
:param bins: list of bin edges
|
274
|
+
:return: pandas DataFrame with the counts of values in each bin
|
275
|
+
"""
|
276
|
+
|
277
|
+
if self._values is None:
|
278
|
+
self.compute_values()
|
279
|
+
|
280
|
+
if self._values is None:
|
281
|
+
raise ValueError("No values computed. Please call compute_values() first.")
|
282
|
+
|
283
|
+
values = self.values('Values')
|
284
|
+
if values is None or len(values) == 0:
|
285
|
+
raise ValueError("No values to distribute. Please compute values first.")
|
286
|
+
|
287
|
+
counts, __ = np.histogram(values, bins=bins)
|
288
|
+
return pd.DataFrame({'Bin Edges': bins[:-1], 'Counts': counts})
|
156
289
|
|
157
290
|
class Array_analysis_polygons():
|
158
291
|
""" Class for values analysis of an array based on a polygon.
|
@@ -164,21 +297,164 @@ class Array_analysis_polygons():
|
|
164
297
|
Plots of the values distribution can be generated using seaborn or plotly.
|
165
298
|
"""
|
166
299
|
|
167
|
-
def __init__(self, wa:WolfArray, polygons:zone):
|
300
|
+
def __init__(self, wa:WolfArray, polygons:zone, buffer_size:float = 0.0):
|
168
301
|
""" Initialize the class with a WolfArray and a zone of polygons """
|
169
302
|
|
170
303
|
self._wa = wa
|
171
|
-
self._polygons = polygons
|
304
|
+
self._polygons = polygons # pointer to the original zone of polygons
|
305
|
+
self._check_names()
|
306
|
+
|
307
|
+
self._has_buffer = buffer_size > 0.0
|
308
|
+
|
309
|
+
self._zone = {polygon.myname: Array_analysis_onepolygon(self._wa, polygon, buffer_size) for polygon in self._polygons.myvectors if polygon.used}
|
310
|
+
|
311
|
+
self._active_categories = self.all_categories
|
312
|
+
|
313
|
+
def as_zone(self, add_values:bool = True) -> zone:
|
314
|
+
""" Convert the analysis to a zone of polygons """
|
315
|
+
|
316
|
+
ret_zone = zone(name=self._polygons.myname)
|
317
|
+
for name, poly in self._zone.items():
|
318
|
+
if name.split('___')[0] in self._active_categories:
|
319
|
+
if poly.has_values:
|
320
|
+
ret_zone.add_vector(poly.as_vector(add_values), forceparent=True)
|
172
321
|
|
173
|
-
|
322
|
+
return ret_zone
|
174
323
|
|
175
|
-
|
324
|
+
@property
|
325
|
+
def _areas(self) -> list[float]:
|
326
|
+
""" Get the areas of the polygons in the zone """
|
327
|
+
return [poly.area for poly in self.polygons.myvectors if poly.used]
|
328
|
+
|
329
|
+
@property
|
330
|
+
def all_categories(self) -> list[str]:
|
331
|
+
""" Get the name of the building categories from the Polygons """
|
332
|
+
|
333
|
+
return list(set([v.myname.split('___')[0] for v in self._polygons.myvectors if v.used]))
|
334
|
+
|
335
|
+
@property
|
336
|
+
def active_categories(self) -> list[str]:
|
337
|
+
""" Get the active categories for the analysis """
|
338
|
+
return self._active_categories
|
339
|
+
|
340
|
+
@active_categories.setter
|
341
|
+
def active_categories(self, categories:list[str]):
|
342
|
+
""" Set the active categories for the analysis
|
343
|
+
|
344
|
+
:param categories: list of categories to activate
|
345
|
+
"""
|
346
|
+
if not categories:
|
347
|
+
raise ValueError("The list of categories must not be empty.")
|
348
|
+
|
349
|
+
all_categories = self.all_categories
|
350
|
+
for cat in categories:
|
351
|
+
if cat not in all_categories:
|
352
|
+
logging.debug(f"Category '{cat}' is not a valid category.")
|
353
|
+
|
354
|
+
self._active_categories = categories
|
355
|
+
|
356
|
+
def activate_category(self, category_name:str):
|
357
|
+
""" Activate a category for the analysis
|
358
|
+
|
359
|
+
:param category_name: name of the category to activate
|
360
|
+
"""
|
361
|
+
if category_name not in self.all_categories:
|
362
|
+
raise ValueError(f"Category '{category_name}' is not a valid category. Available categories: {self.all_categories}")
|
363
|
+
|
364
|
+
if category_name not in self._active_categories:
|
365
|
+
self._active_categories.append(category_name)
|
366
|
+
|
367
|
+
def deactivate_category(self, category_name:str):
|
368
|
+
""" Deactivate a category for the analysis
|
369
|
+
|
370
|
+
:param category_name: name of the category to deactivate
|
371
|
+
"""
|
372
|
+
if category_name not in self._active_categories:
|
373
|
+
raise ValueError(f"Category '{category_name}' is not active. Active categories: {self._active_categories}")
|
374
|
+
|
375
|
+
self._active_categories.remove(category_name)
|
376
|
+
|
377
|
+
def _check_names(self):
|
378
|
+
""" Check if the names of the polygons are unique """
|
379
|
+
names = [poly.myname for poly in self._polygons.myvectors if poly.used]
|
380
|
+
if len(names) != len(set(names)):
|
381
|
+
raise ValueError("Polygon names must be unique. Please rename the polygons in the zone.")
|
382
|
+
|
383
|
+
def __getitem__(self, key:str) -> Array_analysis_onepolygon:
|
176
384
|
""" Get the polygon by name """
|
177
385
|
if key in self._zone:
|
178
386
|
return self._zone[key]
|
179
387
|
else:
|
180
388
|
raise KeyError(f"Polygon {key} not found in zone.")
|
181
389
|
|
390
|
+
def reset_selection(self):
|
391
|
+
""" Reset the selection of cells in all polygons """
|
392
|
+
for poly in self._zone.values():
|
393
|
+
poly.reset_selection()
|
394
|
+
|
395
|
+
def get_values(self) -> pd.DataFrame:
|
396
|
+
""" Get the values of all polygons in the zones as a pandas DataFrame.
|
397
|
+
|
398
|
+
One column per polygon with the values."""
|
399
|
+
|
400
|
+
lst = [pol.values('Values') for key, pol in self._zone.items() if pol.has_values and key.split('___')[0] in self._active_categories]
|
401
|
+
return pd.concat(lst, axis=1)
|
402
|
+
|
403
|
+
def get_geometries(self) -> pd.DataFrame:
|
404
|
+
""" Get the centroids of all polygons in the zone as a pandas DataFrame.
|
405
|
+
|
406
|
+
:return: pandas DataFrame with the centroids of the polygons
|
407
|
+
"""
|
408
|
+
centroids = {key: {'Centroid' : poly.centroid, 'X': poly.centroid.x, 'Y' : poly.centroid.y, 'Geometry': poly._polygon.polygon} for key, poly in self._zone.items() if poly.has_values and key.split('___')[0] in self._active_categories}
|
409
|
+
return pd.DataFrame.from_dict(centroids, orient='index')
|
410
|
+
|
411
|
+
def get_geodataframe_with_values(self, epsg:int = 31370) -> 'gpd.GeoDataFrame':
|
412
|
+
""" Create a GeoDataFrame with the centroids and values of the polygons.
|
413
|
+
|
414
|
+
Values are added as a column named 'Values' as Numpy array."""
|
415
|
+
|
416
|
+
import geopandas as gpd
|
417
|
+
|
418
|
+
geom = self.get_geometries()
|
419
|
+
# Add values as numpy arrays to the DataFrame
|
420
|
+
geom['Values'] = None
|
421
|
+
geom['Values'] = geom['Values'].astype(object)
|
422
|
+
|
423
|
+
# Get values for each polygon and add them to the DataFrame
|
424
|
+
for key, poly in self._zone.items():
|
425
|
+
if poly.has_values and key.split('___')[0] in self._active_categories:
|
426
|
+
values = poly.values('Values')
|
427
|
+
geom.at[key, 'Values'] = values.to_numpy().ravel()
|
428
|
+
|
429
|
+
# Create a GeoDataFrame
|
430
|
+
gdf = gpd.GeoDataFrame(geom, geometry='Geometry', crs=f'EPSG:{epsg}')
|
431
|
+
return gdf
|
432
|
+
|
433
|
+
@property
|
434
|
+
def polygons(self) -> zone:
|
435
|
+
""" Get the zone of polygons """
|
436
|
+
if self._has_buffer:
|
437
|
+
# return a new zone with the polygons and their buffers
|
438
|
+
ret_zone = zone(name = self._polygons.myname)
|
439
|
+
for name, poly in self._zone.items():
|
440
|
+
if name.split('___')[0] in self._active_categories:
|
441
|
+
ret_zone.add_vector(poly._polygon)
|
442
|
+
|
443
|
+
return ret_zone
|
444
|
+
else:
|
445
|
+
# return the original zone of polygons
|
446
|
+
return self._polygons
|
447
|
+
|
448
|
+
@property
|
449
|
+
def keys(self) -> list[str]:
|
450
|
+
""" Get the names of the polygons in the zone """
|
451
|
+
return list(self._zone.keys())
|
452
|
+
|
453
|
+
def update_values(self):
|
454
|
+
""" Update the polygons values in the zone """
|
455
|
+
for poly in self._zone.values():
|
456
|
+
poly.compute_values()
|
457
|
+
|
182
458
|
def plot_values(self, show:bool = True, bins:int = 100,
|
183
459
|
engine:Literal['seaborn', 'plotly'] = 'seaborn'):
|
184
460
|
""" Plot a histogram of the values """
|
@@ -190,13 +466,902 @@ class Array_analysis_polygons():
|
|
190
466
|
|
191
467
|
def plot_values_seaborn(self, bins:int = 100, show:bool = True):
|
192
468
|
""" Plot a histogram of the values """
|
193
|
-
return {key: pol.plot_values_seaborn(bins=bins, show=show) for key, pol in self._zone.items()}
|
469
|
+
return {key: pol.plot_values_seaborn(bins=bins, show=show) for key, pol in self._zone.items() if key.split('___')[0] in self._active_categories}
|
194
470
|
|
195
471
|
def plot_values_plotly(self, bins:int = 100, show:bool = True):
|
196
472
|
""" Plot a histogram of the values """
|
197
473
|
|
198
|
-
return {key: pol.plot_values_plotly(bins=bins, show=show) for key, pol in self._zone.items()}
|
474
|
+
return {key: pol.plot_values_plotly(bins=bins, show=show) for key, pol in self._zone.items() if key.split('___')[0] in self._active_categories}
|
475
|
+
|
476
|
+
def count_strictly_positive(self) -> int:
|
477
|
+
""" Count the number of polygons with values greater than zero """
|
478
|
+
nb = 0
|
479
|
+
for key, poly in self._zone.items():
|
480
|
+
if key.split('___')[0] not in self._active_categories:
|
481
|
+
continue
|
482
|
+
if poly.has_strictly_positive_values:
|
483
|
+
nb += 1
|
484
|
+
return nb
|
485
|
+
|
486
|
+
def values(self, which:Literal['Mean', 'Std', 'Median', 'Sum', 'Volume', 'Area']) -> pd.Series:
|
487
|
+
""" Get the values as a pandas DataFrame
|
488
|
+
|
489
|
+
:param which: Mean, Std, Median, Sum, Volume
|
490
|
+
:return: pandas DataFrame with the values for each polygon
|
491
|
+
"""
|
492
|
+
|
493
|
+
authorized = ['Mean', 'Std', 'Median', 'Sum', 'Volume', 'Area']
|
494
|
+
if which not in authorized:
|
495
|
+
raise ValueError(f"Invalid value for 'which'. Must be one of {authorized}.")
|
496
|
+
|
497
|
+
values = {name: poly.values(which) for name, poly in self._zone.items() if poly.has_values and name.split('___')[0] in self._active_categories}
|
498
|
+
|
499
|
+
if not values:
|
500
|
+
raise ValueError("No values computed. Please compute values first.")
|
501
|
+
|
502
|
+
return pd.Series(values)
|
503
|
+
|
504
|
+
def distribute_polygons(self, bins:list[float], operator:Literal['Mean', 'Median', 'Sum', 'Volume', 'Area']) -> pd.DataFrame:
|
505
|
+
""" Distribute the values of each polygon in bins
|
506
|
+
|
507
|
+
:param bins: list of bin edges
|
508
|
+
:param operator: 'Mean', 'Median', 'Sum', 'Volume', 'Area'
|
509
|
+
:return: pandas DataFrame with the counts of values in each bin for each polygon
|
510
|
+
"""
|
511
|
+
|
512
|
+
values = np.asarray([poly.values(operator) for key, poly in self._zone.items() if poly.has_values and key.split('___')[0] in self._active_categories] )
|
513
|
+
|
514
|
+
if values.size == 0:
|
515
|
+
raise ValueError("No values to distribute. Please compute values first.")
|
516
|
+
|
517
|
+
counts, __ = np.histogram(values, bins=bins)
|
518
|
+
distribution = pd.DataFrame({'Bin Edges': bins[:-1], 'Counts': counts})
|
519
|
+
|
520
|
+
return distribution
|
521
|
+
|
522
|
+
def plot_distributed_values(self, bins:list[float],
|
523
|
+
operator:Literal['Mean', 'Median', 'Sum', 'Volume', 'Area'],
|
524
|
+
show:bool = True,
|
525
|
+
engine:Literal['seaborn', 'plotly'] = 'seaborn'):
|
526
|
+
""" Plot the distribution of values in bins for each polygon
|
527
|
+
|
528
|
+
:param bins: list of bin edges
|
529
|
+
:param operator: 'Mean', 'Median', 'Sum', 'Volume', 'Area'
|
530
|
+
:param show: whether to show the plot
|
531
|
+
:param engine: 'seaborn' or 'plotly'
|
532
|
+
"""
|
533
|
+
|
534
|
+
distribution = self.distribute_polygons(bins, operator)
|
535
|
+
|
536
|
+
if engine == 'seaborn':
|
537
|
+
fig, ax = self.plot_distribution_seaborn(distribution, show=show)
|
538
|
+
elif engine == 'plotly':
|
539
|
+
fig, ax = self.plot_distribution_plotly(distribution, show=show)
|
540
|
+
|
541
|
+
ax.set_title(f'Distribution of Values ({operator})')
|
542
|
+
|
543
|
+
return fig, ax
|
544
|
+
|
545
|
+
def plot_distribution_seaborn(self, distribution:pd.DataFrame, show:bool = True):
|
546
|
+
""" Plot the distribution of values in bins using seaborn
|
547
|
+
|
548
|
+
:param distribution: pandas DataFrame with the counts of values in each bin
|
549
|
+
:param show: whether to show the plot
|
550
|
+
"""
|
551
|
+
|
552
|
+
import seaborn as sns
|
553
|
+
import matplotlib.pyplot as plt
|
554
|
+
|
555
|
+
fig, ax = plt.subplots()
|
556
|
+
sns.barplot(x='Bin Edges', y='Counts', data=distribution, ax=ax)
|
557
|
+
|
558
|
+
ax.set_xlabel('Bin Edges')
|
559
|
+
ax.set_ylabel('Counts')
|
560
|
+
ax.set_title('Distribution of Values')
|
561
|
+
|
562
|
+
if show:
|
563
|
+
plt.show()
|
564
|
+
|
565
|
+
return (fig, ax)
|
566
|
+
|
567
|
+
def plot_distribution_plotly(self, distribution:pd.DataFrame, show:bool = True):
|
568
|
+
""" Plot the distribution of values in bins using plotly
|
569
|
+
|
570
|
+
:param distribution: pandas DataFrame with the counts of values in each bin
|
571
|
+
:param show: whether to show the plot
|
572
|
+
"""
|
573
|
+
|
574
|
+
import plotly.express as px
|
575
|
+
|
576
|
+
fig = px.bar(distribution, x='Bin Edges', y='Counts',
|
577
|
+
title='Distribution of Values')
|
578
|
+
|
579
|
+
fig.update_layout(xaxis_title='Bin Edges', yaxis_title='Counts')
|
580
|
+
|
581
|
+
if show:
|
582
|
+
fig.show(renderer='browser')
|
583
|
+
|
584
|
+
return fig
|
585
|
+
|
586
|
+
def clustering(self, n_clusters:int = 5, operator:Literal['Mean', 'Median', 'Sum', 'Volume', 'Area'] = 'Mean'):
|
587
|
+
""" Perform clustering on the polygons based on their values. """
|
588
|
+
from sklearn.cluster import KMeans
|
589
|
+
import geopandas as gpd
|
590
|
+
|
591
|
+
# Get the values of the polygons
|
592
|
+
values = self.values(operator)
|
593
|
+
geometries = self.get_geodataframe_with_values()
|
594
|
+
|
595
|
+
xy = geometries[['X', 'Y']].copy()
|
596
|
+
|
597
|
+
if values.empty:
|
598
|
+
raise ValueError("No values to cluster. Please compute values first.")
|
599
|
+
|
600
|
+
# Perform clustering
|
601
|
+
kmeans = KMeans(n_clusters=n_clusters)
|
602
|
+
|
603
|
+
kmeans.fit(xy[['X', 'Y']].values)
|
604
|
+
labels = kmeans.labels_
|
605
|
+
geometries['Cluster'] = labels
|
606
|
+
|
607
|
+
centroids = kmeans.cluster_centers_
|
608
|
+
cluster_centroids = [Point(xy) for xy in centroids]
|
609
|
+
|
610
|
+
# Find footprints of the clusters
|
611
|
+
footprints = []
|
612
|
+
for label in np.unique(labels):
|
613
|
+
geom_cluster = geometries[geometries['Cluster'] == label]
|
614
|
+
footprint = geom_cluster.geometry.unary_union.convex_hull
|
615
|
+
footprints.append(footprint)
|
616
|
+
|
617
|
+
return geometries, (cluster_centroids, footprints)
|
618
|
+
|
619
|
+
|
620
|
+
class Array_analysis_zones():
|
621
|
+
""" Class for values analysis of an array based on a Zones instance.
|
622
|
+
|
623
|
+
This class select values insides a zone of polygons and plot statistics of the values.
|
624
|
+
"""
|
625
|
+
|
626
|
+
def __init__(self, wa:WolfArray, zones:Zones, buffer_size:float = 0.0):
|
627
|
+
""" Initialize the class with a Wolf Zones """
|
628
|
+
|
629
|
+
self._wa = wa
|
630
|
+
self._zones = zones
|
631
|
+
|
632
|
+
self._polygons = {zone.myname: Array_analysis_polygons(self._wa, zone, buffer_size) for zone in self._zones.myzones if zone.used}
|
633
|
+
|
634
|
+
def as_zones(self, add_values:bool = True) -> Zones:
|
635
|
+
""" Convert the analysis to a Zones instance """
|
636
|
+
|
637
|
+
newzones = Zones(idx=self._zones.idx)
|
638
|
+
for name, pol in self._polygons.items():
|
639
|
+
newzones.add_zone(pol.as_zone(add_values), forceparent=True)
|
640
|
+
|
641
|
+
return newzones
|
642
|
+
|
643
|
+
|
644
|
+
def __getitem__(self, key:str) -> Array_analysis_polygons:
|
645
|
+
""" Get the zone by name """
|
646
|
+
if key in self._polygons:
|
647
|
+
return self._polygons[key]
|
648
|
+
else:
|
649
|
+
raise KeyError(f"Zone {key} not found in zones.")
|
650
|
+
|
651
|
+
def reset_selection(self):
|
652
|
+
""" Reset the selection of cells in all polygons """
|
653
|
+
for poly in self._polygons.values():
|
654
|
+
poly.reset_selection()
|
655
|
+
|
656
|
+
@property
|
657
|
+
def keys(self) -> list[str]:
|
658
|
+
""" Get the names of the polygons in the zones """
|
659
|
+
return list(self._polygons.keys())
|
660
|
+
|
661
|
+
def update_values(self):
|
662
|
+
""" Update the polygons values in the zones """
|
663
|
+
for pol in self._polygons.values():
|
664
|
+
pol.update_values()
|
665
|
+
|
666
|
+
def get_values(self) -> dict[str, pd.DataFrame]:
|
667
|
+
""" Get the values of all polygons in the zones as a dictionary of pandas DataFrames """
|
668
|
+
|
669
|
+
return {key: pol.get_values() for key, pol in self._polygons.items()}
|
670
|
+
|
671
|
+
def plot_values(self, show:bool = True, bins:int = 100,
|
672
|
+
engine:Literal['seaborn', 'plotly'] = 'seaborn'):
|
673
|
+
""" Plot a histogram of the values """
|
674
|
+
if engine == 'seaborn':
|
675
|
+
return self.plot_values_seaborn(show=show, bins=bins)
|
676
|
+
elif engine == 'plotly':
|
677
|
+
return self.plot_values_plotly(show=show, bins=bins)
|
678
|
+
|
679
|
+
def plot_values_seaborn(self, bins:int = 100, show:bool = True):
|
680
|
+
""" Plot a histogram of the values """
|
681
|
+
return {key: pol.plot_values_seaborn(bins=bins, show=show) for key, pol in self._polygons.items()}
|
682
|
+
|
683
|
+
def plot_values_plotly(self, bins:int = 100, show:bool = True):
|
684
|
+
""" Plot a histogram of the values """
|
685
|
+
return {key: pol.plot_values_plotly(bins=bins, show=show) for key, pol in self._polygons.items()}
|
686
|
+
|
687
|
+
def distribute_zones(self, bins:list[float],
|
688
|
+
operator:Literal['Mean', 'Median', 'Sum', 'Volume', 'Area']) -> dict[str, pd.DataFrame]:
|
689
|
+
""" Distribute the values of each zone in bins
|
690
|
+
|
691
|
+
:param bins: list of bin edges
|
692
|
+
:param operator: 'Mean', 'Median', 'Sum', 'Volume', 'Area'
|
693
|
+
:return: pandas DataFrame with the counts of values in each bin for each zone
|
694
|
+
"""
|
695
|
+
|
696
|
+
return {key: pol.distribute_polygons(bins, operator) for key, pol in self._polygons.items()}
|
697
|
+
|
698
|
+
def values(self, which:Literal['Mean', 'Std', 'Median', 'Sum', 'Volume', 'Area']) -> dict[str, pd.Series]:
|
699
|
+
""" Get the values as a dictionnary of pandas Series
|
700
|
+
|
701
|
+
:param which: Mean, Std, Median, Sum, Volume
|
702
|
+
:return: pandas DataFrame with the values for each polygon
|
703
|
+
"""
|
704
|
+
|
705
|
+
authorized = ['Mean', 'Std', 'Median', 'Sum', 'Volume', 'Area']
|
706
|
+
if which not in authorized:
|
707
|
+
raise ValueError(f"Invalid value for 'which'. Must be one of {authorized}.")
|
708
|
+
|
709
|
+
values = {name: pol.values(which) for name, pol in self._polygons.items()}
|
710
|
+
|
711
|
+
if not values:
|
712
|
+
raise ValueError("No values computed. Please compute values first.")
|
713
|
+
|
714
|
+
return values
|
715
|
+
|
716
|
+
class Arrays_analysis_zones():
|
717
|
+
"""
|
718
|
+
Class for analysis multiples arrays based on a Zones instance.
|
719
|
+
Each array must have the same shape.
|
720
|
+
"""
|
721
|
+
|
722
|
+
def __init__(self, arrays:dict[str, WolfArray], zones:Zones, buffer_size:float = 0.0):
|
723
|
+
""" Initialize the class with a list of WolfArray and a Zones instance """
|
724
|
+
if not arrays:
|
725
|
+
raise ValueError("The list of arrays must not be empty.")
|
726
|
+
|
727
|
+
self._arrays = arrays
|
728
|
+
self._zones = zones
|
729
|
+
self._xlabel = 'Value'
|
730
|
+
|
731
|
+
# Check that all arrays have the same shape
|
732
|
+
ref = next(iter(arrays.values()))
|
733
|
+
for array in arrays.values():
|
734
|
+
if not array.is_like(ref):
|
735
|
+
raise ValueError("All arrays must have the same shape.")
|
736
|
+
|
737
|
+
self._polygons = {zone.myname: {key: Array_analysis_polygons(array, zone, buffer_size) for key, array in self._arrays.items()}
|
738
|
+
for zone in self._zones.myzones if zone.used }
|
739
|
+
|
740
|
+
self._active_categories = self.all_categories
|
741
|
+
self._active_arrays = self.all_arrays
|
742
|
+
|
743
|
+
def __getitem__(self, key:str | tuple) -> Array_analysis_polygons:
|
744
|
+
""" Get the zone by name """
|
745
|
+
if isinstance(key, tuple):
|
746
|
+
if len(key) != 2:
|
747
|
+
raise ValueError("Key must be a tuple of (zone_name, array_name).")
|
748
|
+
zone_name, array_name = key
|
749
|
+
if zone_name not in self._polygons or array_name not in self._polygons[zone_name]:
|
750
|
+
raise KeyError(f"Zone {zone_name} or array {array_name} not found in zones.")
|
751
|
+
return self._polygons[zone_name][array_name]
|
752
|
+
|
753
|
+
elif isinstance(key, str):
|
754
|
+
if len(self._polygons) == 1:
|
755
|
+
# If there is only one zone, return the first array in that zone
|
756
|
+
zone_name = next(iter(self._polygons))
|
757
|
+
if key in self._polygons[zone_name]:
|
758
|
+
return self._polygons[zone_name][key]
|
759
|
+
else:
|
760
|
+
raise KeyError(f"Array {key} not found in the only zone available.")
|
761
|
+
else:
|
762
|
+
if key in self._polygons:
|
763
|
+
return self._polygons[key]
|
764
|
+
else:
|
765
|
+
raise KeyError(f"Zone {key} not found in zones.")
|
766
|
+
|
767
|
+
def as_zones(self, add_values:bool = True) -> Zones:
|
768
|
+
""" Convert the analysis to a Zones instance """
|
769
|
+
|
770
|
+
newzones = Zones(idx=self._zones.idx)
|
771
|
+
for name, dct in self._polygons.items():
|
772
|
+
for array_name, pols in dct.items():
|
773
|
+
if array_name not in self._active_arrays:
|
774
|
+
continue
|
775
|
+
|
776
|
+
newzone = pols.as_zone(add_values)
|
777
|
+
newzone.myname = f"{array_name}"
|
778
|
+
|
779
|
+
newzones.add_zone(newzone, forceparent=True)
|
780
|
+
|
781
|
+
return newzones
|
782
|
+
|
783
|
+
@property
|
784
|
+
def _areas(self) -> dict[str, list[float]]:
|
785
|
+
""" Get the areas of the polygons in the zones """
|
786
|
+
return {polygons.myname: [poly.area for poly in polygons.myvectors if poly.used]
|
787
|
+
for polygons in self._zones.myzones}
|
788
|
+
|
789
|
+
@property
|
790
|
+
def all_categories(self) -> list[str]:
|
791
|
+
""" Get the name of the building categories from the Zones """
|
792
|
+
|
793
|
+
return sorted(list(set([v.myname.split('___')[0] for z in self._zones.myzones for v in z.myvectors])))
|
794
|
+
|
795
|
+
@property
|
796
|
+
def all_arrays(self) -> list[str]:
|
797
|
+
""" Get the names of all arrays """
|
798
|
+
return list(self._arrays.keys())
|
799
|
+
|
800
|
+
def activate_array(self, array_name:str):
|
801
|
+
""" Activate an array for the analysis
|
802
|
+
|
803
|
+
:param array_name: name of the array to activate
|
804
|
+
"""
|
805
|
+
if array_name not in self.all_arrays:
|
806
|
+
raise ValueError(f"Array '{array_name}' is not a valid array. Available arrays: {self.all_arrays}")
|
807
|
+
|
808
|
+
if array_name not in self._active_arrays:
|
809
|
+
self._active_arrays.append(array_name)
|
810
|
+
|
811
|
+
def deactivate_array(self, array_name:str):
|
812
|
+
""" Deactivate an array for the analysis
|
813
|
+
|
814
|
+
:param array_name: name of the array to deactivate
|
815
|
+
"""
|
816
|
+
if array_name not in self._active_arrays:
|
817
|
+
raise ValueError(f"Array '{array_name}' is not active. Active arrays: {self._active_arrays}")
|
818
|
+
|
819
|
+
self._active_arrays.remove(array_name)
|
820
|
+
|
821
|
+
def activate_category(self, category_name:str):
|
822
|
+
""" Activate a category for the analysis
|
823
|
+
|
824
|
+
:param category_name: name of the category to activate
|
825
|
+
"""
|
826
|
+
if category_name not in self.all_categories:
|
827
|
+
raise ValueError(f"Category '{category_name}' is not a valid category. Available categories: {self.all_categories}")
|
828
|
+
|
829
|
+
if category_name not in self._active_categories:
|
830
|
+
self._active_categories.append(category_name)
|
831
|
+
|
832
|
+
for zone_name, dct in self._polygons.items():
|
833
|
+
for array_name, poly in dct.items():
|
834
|
+
poly.active_categories = self._active_categories
|
835
|
+
|
836
|
+
def deactivate_category(self, category_name:str):
|
837
|
+
""" Deactivate a category for the analysis
|
838
|
+
|
839
|
+
:param category_name: name of the category to deactivate
|
840
|
+
"""
|
841
|
+
if category_name not in self._active_categories:
|
842
|
+
raise ValueError(f"Category '{category_name}' is not active. Active categories: {self._active_categories}")
|
843
|
+
|
844
|
+
self._active_categories.remove(category_name)
|
845
|
+
|
846
|
+
for zone_name, dct in self._polygons.items():
|
847
|
+
for array_name, poly in dct.items():
|
848
|
+
poly.active_categories = self._active_categories
|
849
|
+
|
850
|
+
@property
|
851
|
+
def active_arrays(self) -> list[str]:
|
852
|
+
""" Get the active arrays for the analysis """
|
853
|
+
return self._active_arrays
|
854
|
+
|
855
|
+
@active_arrays.setter
|
856
|
+
def active_arrays(self, arrays:list[str]):
|
857
|
+
""" Set the active arrays for the analysis
|
858
|
+
|
859
|
+
:param arrays: list of arrays to activate
|
860
|
+
"""
|
861
|
+
if not arrays:
|
862
|
+
raise ValueError("The list of arrays must not be empty.")
|
863
|
+
|
864
|
+
all_arrays = self.all_arrays
|
865
|
+
for arr in arrays:
|
866
|
+
if arr not in all_arrays:
|
867
|
+
raise ValueError(f"Array '{arr}' is not a valid array. Available arrays: {all_arrays}")
|
868
|
+
|
869
|
+
self._active_arrays = arrays
|
870
|
+
|
871
|
+
@property
|
872
|
+
def active_categories(self) -> list[str]:
|
873
|
+
""" Get the active categories for the analysis """
|
874
|
+
return self._active_categories
|
875
|
+
|
876
|
+
@active_categories.setter
|
877
|
+
def active_categories(self, categories:list[str]):
|
878
|
+
""" Set the active categories for the analysis
|
879
|
+
|
880
|
+
:param categories: list of categories to activate
|
881
|
+
"""
|
882
|
+
if not categories:
|
883
|
+
raise ValueError("The list of categories must not be empty.")
|
884
|
+
|
885
|
+
all_categories = self.all_categories
|
886
|
+
for cat in categories:
|
887
|
+
if cat not in all_categories:
|
888
|
+
raise ValueError(f"Category '{cat}' is not a valid category. Available categories: {all_categories}")
|
889
|
+
|
890
|
+
self._active_categories = categories
|
891
|
+
|
892
|
+
for zone_name, dct in self._polygons.items():
|
893
|
+
for array_name, poly in dct.items():
|
894
|
+
poly.active_categories = self._active_categories
|
895
|
+
|
896
|
+
def get_values(self) -> dict[str, dict[str, pd.DataFrame]]:
|
897
|
+
""" Get the values of all polygons in the zones as a dictionary of pandas DataFrames """
|
898
|
+
|
899
|
+
values = {}
|
900
|
+
for zone_name, polygons in self._polygons.items():
|
901
|
+
values[zone_name] = {}
|
902
|
+
for array_name, polygon in polygons.items():
|
903
|
+
values[zone_name][array_name] = polygon.get_values()
|
904
|
+
|
905
|
+
return values
|
906
|
+
|
907
|
+
def values(self, which:Literal['Mean', 'Std', 'Median', 'Sum', 'Volume', 'Area']) -> dict[str, dict[str, pd.Series]]:
|
908
|
+
""" Get the values of all polygons in the zones as a dictionary of pandas Series
|
909
|
+
|
910
|
+
:param which: Mean, Std, Median, Sum, Volume, Area
|
911
|
+
:return: dictionary with zone names as keys and dictionaries of array names and their values as values
|
912
|
+
"""
|
913
|
+
|
914
|
+
authorized = ['Mean', 'Std', 'Median', 'Sum', 'Volume', 'Area']
|
915
|
+
if which not in authorized:
|
916
|
+
raise ValueError(f"Invalid value for 'which'. Must be one of {authorized}.")
|
917
|
+
|
918
|
+
values = {}
|
919
|
+
for zone_name, polygons in self._polygons.items():
|
920
|
+
values[zone_name] = {}
|
921
|
+
for array_name, polygon in polygons.items():
|
922
|
+
if array_name not in self._active_arrays:
|
923
|
+
continue
|
924
|
+
values[zone_name][array_name] = polygon.values(which)
|
925
|
+
|
926
|
+
return values
|
927
|
+
|
928
|
+
def update_values(self):
|
929
|
+
""" Update the polygons values in the zones for all arrays """
|
930
|
+
for polygons in self._polygons.values():
|
931
|
+
for polygon in polygons.values():
|
932
|
+
polygon.update_values()
|
933
|
+
|
934
|
+
def count_strictly_positive(self) -> dict[str, int]:
|
935
|
+
""" Count the number of polygons with values greater than zero for each array """
|
936
|
+
import concurrent.futures
|
937
|
+
|
938
|
+
counts = {}
|
939
|
+
for zone_name, polygons in self._polygons.items():
|
940
|
+
counts[zone_name] = {}
|
941
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
942
|
+
futures = {}
|
943
|
+
for array_name, polygon in polygons.items():
|
944
|
+
if array_name not in self._active_arrays:
|
945
|
+
continue
|
946
|
+
futures[array_name] = executor.submit(polygon.count_strictly_positive)
|
947
|
+
for array_name, future in futures.items():
|
948
|
+
counts[zone_name][array_name] = future.result()
|
949
|
+
|
950
|
+
return counts
|
951
|
+
|
952
|
+
def count_strictly_positive_as_df(self, merge_zones: bool = False) -> pd.DataFrame:
|
953
|
+
""" Count the number of polygons with strictly positive values for each array as a pandas DataFrame
|
954
|
+
|
955
|
+
:return: pandas DataFrame with the counts of strictly positive values for each array in each zone
|
956
|
+
"""
|
957
|
+
|
958
|
+
counts = self.count_strictly_positive()
|
959
|
+
|
960
|
+
df = pd.DataFrame({'Zone': zone_name, 'Array': array_name, 'Count': count}
|
961
|
+
for zone_name, arrays in counts.items()
|
962
|
+
for array_name, count in arrays.items())
|
963
|
+
if merge_zones:
|
964
|
+
# Sum counts across zones for each array
|
965
|
+
df = df.groupby('Array', as_index=False).sum()
|
966
|
+
# remove the 'Zone' column
|
967
|
+
df = df.drop(columns=['Zone'])
|
968
|
+
|
969
|
+
return df
|
970
|
+
|
971
|
+
def distribute_zones(self, bins:list[float],
|
972
|
+
operator:Literal['Mean', 'Median', 'Sum', 'Volume', 'Area']) -> dict[str, dict[str, pd.DataFrame]]:
|
973
|
+
""" Distribute the values of each zone in bins for each array
|
974
|
+
|
975
|
+
:param bins: list of bin edges
|
976
|
+
:param operator: 'Mean', 'Median', 'Sum', 'Volume', 'Area'
|
977
|
+
:return: dictionary with zone names as keys and dictionaries of array names and their distributions as values
|
978
|
+
"""
|
979
|
+
|
980
|
+
distributions = {}
|
981
|
+
for zone_name, polygons in self._polygons.items():
|
982
|
+
distributions[zone_name] = {}
|
983
|
+
for array_name, polygon in polygons.items():
|
984
|
+
if array_name not in self._active_arrays:
|
985
|
+
continue
|
986
|
+
distributions[zone_name][array_name] = polygon.distribute_polygons(bins, operator)
|
987
|
+
|
988
|
+
return distributions
|
989
|
+
|
990
|
+
def distribute_zones_as_df(self, bins:list[float],
|
991
|
+
operator:Literal['Mean', 'Median', 'Sum', 'Volume', 'Area'],
|
992
|
+
merge_zones:bool = False) -> pd.DataFrame:
|
993
|
+
""" Distribute the values of each zone in bins for each array as a pandas DataFrame.
|
994
|
+
|
995
|
+
Date are tabulated in a DataFrame with columns 'Zone', 'Array', 'Bin Edges', 'Count'.
|
996
|
+
It is more convenient for plotting and analysis.
|
997
|
+
|
998
|
+
:param bins: list of bin edges
|
999
|
+
:param operator: 'Mean', 'Median', 'Sum', 'Volume', 'Area'
|
1000
|
+
:return: pandas DataFrame with the counts of values in each bin for each array in each zone
|
1001
|
+
"""
|
1002
|
+
|
1003
|
+
distributions = self.distribute_zones(bins, operator)
|
1004
|
+
df = pd.DataFrame({'Zone': zone_name, 'Array': array, 'Bin Edges': bin, 'Count': count}
|
1005
|
+
for zone_name, arrays in distributions.items()
|
1006
|
+
for array, bins_counts in arrays.items()
|
1007
|
+
for bin, count in zip(bins_counts['Bin Edges'], bins_counts['Counts']))
|
1008
|
+
if merge_zones:
|
1009
|
+
# Sum counts across zones for each array
|
1010
|
+
df = df.groupby(['Array', 'Bin Edges'], as_index=False).sum()
|
1011
|
+
# remove the 'Zone' column
|
1012
|
+
df = df.drop(columns=['Zone'])
|
1013
|
+
return df
|
1014
|
+
|
1015
|
+
def _values_as_df(self, which:Literal['Mean', 'Std', 'Median', 'Sum', 'Volume', 'Area'],
|
1016
|
+
merge_zones:bool = False) -> pd.DataFrame:
|
1017
|
+
""" Get a full DataFrame with all arrays, zones and values for each polygon.
|
1018
|
+
|
1019
|
+
:param merge_zones: whether to merge the zones in the DataFrame
|
1020
|
+
:return: pandas DataFrame with the counts of strictly positive values for each array in each zone
|
1021
|
+
"""
|
1022
|
+
dct = self.values(which)
|
1023
|
+
|
1024
|
+
df = pd.DataFrame({'Zone': zone_name, 'Array': array_name, 'Value': value}
|
1025
|
+
for zone_name, arrays in dct.items()
|
1026
|
+
for array_name, value in arrays.items()
|
1027
|
+
for value in value.values)
|
1028
|
+
if merge_zones:
|
1029
|
+
# remove the 'Zone' column
|
1030
|
+
df = df.drop(columns=['Zone'])
|
1031
|
+
|
1032
|
+
return df
|
1033
|
+
|
1034
|
+
def plot_count_strictly_positive(self, show:bool = True,
|
1035
|
+
engine:Literal['seaborn', 'plotly'] = 'seaborn',
|
1036
|
+
merge_zones:bool = False):
|
1037
|
+
""" Plot the count of strictly positive values for each array in each zone
|
1038
|
+
|
1039
|
+
:param show: whether to show the plot
|
1040
|
+
:param engine: 'seaborn' or 'plotly'
|
1041
|
+
"""
|
1042
|
+
|
1043
|
+
if engine == 'seaborn':
|
1044
|
+
return self._plot_count_strictly_positive_seaborn(show=show, merge_zones=merge_zones)
|
1045
|
+
elif engine == 'plotly':
|
1046
|
+
return self.plot_count_strictly_positive_plotly(show=show, merge_zones=merge_zones)
|
1047
|
+
|
1048
|
+
def _plot_count_strictly_positive_seaborn(self, show:bool = True, merge_zones:bool = False):
|
1049
|
+
""" Plot the count of strictly positive values for each array in each zone using seaborn
|
1050
|
+
|
1051
|
+
:param counts: dictionary with zone names as keys, and dictionaries of array names and their counts as values
|
1052
|
+
:param show: whether to show the plot
|
1053
|
+
"""
|
1054
|
+
|
1055
|
+
import seaborn as sns
|
1056
|
+
import matplotlib.pyplot as plt
|
1057
|
+
|
1058
|
+
df = self.count_strictly_positive_as_df(merge_zones=merge_zones)
|
1059
|
+
|
1060
|
+
fig, ax = plt.subplots()
|
1061
|
+
|
1062
|
+
if merge_zones:
|
1063
|
+
# If merging zones, we only have 'Array' and 'Count' columns
|
1064
|
+
sns.barplot(x='Array', y='Count', data=df, ax=ax)
|
1065
|
+
else:
|
1066
|
+
sns.barplot(x='Array', y='Count', hue='Zone', data=df, ax=ax)
|
1067
|
+
|
1068
|
+
ax.set_xlabel('Array')
|
1069
|
+
ax.set_ylabel('Count of strictly positive values')
|
1070
|
+
ax.set_title('Count of strictly positive values')
|
1071
|
+
plt.tight_layout()
|
1072
|
+
|
1073
|
+
if show:
|
1074
|
+
plt.show()
|
1075
|
+
|
1076
|
+
return (fig, ax)
|
1077
|
+
|
1078
|
+
def _plot_count_strictly_positive_plotly(self, show:bool = True, merge_zones:bool = False):
|
1079
|
+
""" Plot the count of strictly positive values for each array in each zone using plotly
|
1080
|
+
|
1081
|
+
:param counts: dictionary with zone names as keys, and dictionaries of array names and their counts as values
|
1082
|
+
:param show: whether to show the plot
|
1083
|
+
"""
|
1084
|
+
|
1085
|
+
import plotly.express as px
|
1086
|
+
|
1087
|
+
df = self.count_strictly_positive_as_df(merge_zones=merge_zones)
|
1088
|
+
|
1089
|
+
if merge_zones:
|
1090
|
+
fig = px.bar(df, x='Array', y='Count', title='Count of strictly positive values',
|
1091
|
+
labels={'Count': 'Count of strictly positive values'})
|
1092
|
+
else:
|
1093
|
+
fig = px.bar(df, x='Array', y='Count', color='Zone',
|
1094
|
+
title='Count of strictly positive values',
|
1095
|
+
labels={'Count': 'Count of strictly positive values'})
|
1096
|
+
|
1097
|
+
fig.update_layout(xaxis_title='Array', yaxis_title='Count of strictly positive values')
|
1098
|
+
|
1099
|
+
if show:
|
1100
|
+
fig.show(renderer='browser')
|
1101
|
+
|
1102
|
+
return fig
|
1103
|
+
|
1104
|
+
def plot_distributed_values(self, bins:list[float]= [0., .3, 1.3, -1.],
|
1105
|
+
operator:Literal['Mean', 'Median', 'Sum', 'Volume', 'Area'] = 'Median',
|
1106
|
+
show:bool = True,
|
1107
|
+
engine:Literal['seaborn', 'plotly'] = 'seaborn',
|
1108
|
+
merge_zones:bool = False):
|
1109
|
+
""" Plot the distribution of values in bins for each array in each zone or merged zones.
|
1110
|
+
:param bins: list of bin edges
|
1111
|
+
:param operator: 'Mean', 'Median', 'Sum', 'Volume', 'Area'
|
1112
|
+
:param show: whether to show the plot
|
1113
|
+
:param engine: 'seaborn' or 'plotly'
|
1114
|
+
:param merge_zones: whether to merge the zones in the plot
|
1115
|
+
"""
|
1116
|
+
|
1117
|
+
if engine == 'seaborn':
|
1118
|
+
return self._plot_distributed_values_seaborn(bins, operator, show=show, merge_zones=merge_zones)
|
1119
|
+
elif engine == 'plotly':
|
1120
|
+
return self._plot_distributed_values_plotly(bins, operator, show=show, merge_zones=merge_zones)
|
1121
|
+
else:
|
1122
|
+
raise ValueError(f"Invalid engine '{engine}'. Must be 'seaborn' or 'plotly'.")
|
1123
|
+
|
1124
|
+
def _plot_distributed_values_seaborn(self, bins:list[float],
|
1125
|
+
operator:Literal['Mean', 'Median', 'Sum', 'Volume', 'Area'],
|
1126
|
+
show:bool = True,
|
1127
|
+
merge_zones:bool = False):
|
1128
|
+
""" Plot the distribution of values in bins for each array in each zone using seaborn
|
1129
|
+
|
1130
|
+
:param bins: list of bin edges
|
1131
|
+
:param operator: 'Mean', 'Median', 'Sum', 'Volume', 'Area'
|
1132
|
+
:param show: whether to show the plot
|
1133
|
+
:param merge_zones: whether to merge the zones in the plot
|
1134
|
+
"""
|
1135
|
+
|
1136
|
+
import seaborn as sns
|
1137
|
+
import matplotlib.pyplot as plt
|
1138
|
+
|
1139
|
+
df = self._values_as_df(operator, merge_zones=merge_zones)
|
1140
|
+
|
1141
|
+
if bins[-1] == -1:
|
1142
|
+
bins[-1] = df['Value'].max()
|
1143
|
+
|
1144
|
+
if merge_zones:
|
1145
|
+
fig, ax = plt.subplots()
|
1146
|
+
sns.histplot(df, x='Value', hue='Array', multiple='stack', bins = bins, ax=ax)
|
1147
|
+
|
1148
|
+
# set ticks
|
1149
|
+
ax.set_xticks(bins)
|
1150
|
+
ax.set_xticklabels([f"{b:.2f}" for b in bins])
|
1151
|
+
ax.set_xlabel(self._xlabel)
|
1152
|
+
if show:
|
1153
|
+
plt.show()
|
1154
|
+
|
1155
|
+
return (fig, ax)
|
1156
|
+
else:
|
1157
|
+
# 1 plot per zone
|
1158
|
+
figs, axs = [], []
|
1159
|
+
for i, zone_name in enumerate(self._polygons.keys()):
|
1160
|
+
fig, ax = plt.subplots()
|
1161
|
+
sns.histplot(df[df['Zone'] == zone_name],
|
1162
|
+
x='Value',
|
1163
|
+
hue='Array',
|
1164
|
+
multiple='stack', bins=bins, ax=ax)
|
1165
|
+
|
1166
|
+
ax.set_xlabel(self._xlabel)
|
1167
|
+
ax.set_ylabel('Counts')
|
1168
|
+
ax.set_title(f'Distribution of Values ({zone_name})')
|
1169
|
+
|
1170
|
+
# set ticks
|
1171
|
+
ax.set_xticks(bins)
|
1172
|
+
ax.set_xticklabels([f"{b:.2f}" for b in bins])
|
1173
|
+
fig.tight_layout()
|
1174
|
+
|
1175
|
+
figs.append(fig)
|
1176
|
+
axs.append(ax)
|
1177
|
+
|
1178
|
+
if show:
|
1179
|
+
plt.show()
|
1180
|
+
|
1181
|
+
return (figs, axs)
|
1182
|
+
|
1183
|
+
def _plot_distributed_values_plotly(self, bins:list[float],
|
1184
|
+
operator:Literal['Mean', 'Median', 'Sum', 'Volume', 'Area'],
|
1185
|
+
show:bool = True,
|
1186
|
+
merge_zones:bool = False):
|
1187
|
+
""" Plot the distribution of values in bins for each array in each zone using plotly
|
1188
|
+
|
1189
|
+
:param bins: list of bin edges
|
1190
|
+
:param operator: 'Mean', 'Median', 'Sum', 'Volume', 'Area'
|
1191
|
+
:param show: whether to show the plot
|
1192
|
+
:param merge_zones: whether to merge the zones in the plot
|
1193
|
+
"""
|
1194
|
+
|
1195
|
+
import plotly.graph_objects as go
|
1196
|
+
|
1197
|
+
logging.warning("In plotly engine, the bins will be ignored and replaced by 10 automatic bins.")
|
1198
|
+
|
1199
|
+
df = self._values_as_df(operator, merge_zones=merge_zones)
|
1200
|
+
|
1201
|
+
if bins[-1] == -1:
|
1202
|
+
bins[-1] = df['Value'].max()
|
1203
|
+
|
1204
|
+
if merge_zones:
|
1205
|
+
fig = go.Figure()
|
1206
|
+
for array_name in df['Array'].unique():
|
1207
|
+
fig.add_trace(go.Histogram(x=df[df['Array'] == array_name]['Value'],
|
1208
|
+
name=array_name,
|
1209
|
+
histnorm='',
|
1210
|
+
xbins=dict(start=bins[0], end=bins[-1], size=(bins[-1] - bins[0]) / 10)))
|
1211
|
+
else:
|
1212
|
+
fig = go.Figure()
|
1213
|
+
for zone_name in df['Zone'].unique():
|
1214
|
+
for array_name in df['Array'].unique():
|
1215
|
+
fig.add_trace(go.Histogram(x=df[(df['Zone'] == zone_name) & (df['Array'] == array_name)]['Value'],
|
1216
|
+
name=f"{zone_name} - {array_name}",
|
1217
|
+
histnorm='',
|
1218
|
+
xbins=dict(start=bins[0], end=bins[-1], size=(bins[-1] - bins[0]) / 10)))
|
1219
|
+
|
1220
|
+
if show:
|
1221
|
+
fig.show(renderer='browser')
|
1222
|
+
|
1223
|
+
return fig
|
1224
|
+
|
1225
|
+
def _plot_distributed_areas_seaborn(self, bins:list[float],
|
1226
|
+
operator:Literal['Mean', 'Median', 'Sum', 'Volume', 'Area'],
|
1227
|
+
show:bool = True,
|
1228
|
+
merge_zones:bool = False):
|
1229
|
+
""" Plot the distribution of values in bins for each array in each zone using seaborn
|
1230
|
+
|
1231
|
+
:param bins: list of bin edges
|
1232
|
+
:param operator: 'Mean', 'Median', 'Sum', 'Volume', 'Area'
|
1233
|
+
:param show: whether to show the plot
|
1234
|
+
:param merge_zones: whether to merge the zones in the plot
|
1235
|
+
"""
|
1236
|
+
|
1237
|
+
import seaborn as sns
|
1238
|
+
import matplotlib.pyplot as plt
|
1239
|
+
|
1240
|
+
df = self._values_as_df(operator, merge_zones=merge_zones)
|
1241
|
+
df_area = self._values_as_df('Area', merge_zones=merge_zones)
|
1242
|
+
|
1243
|
+
# # Multiply the values by the area to get the weighted distribution
|
1244
|
+
# df['Value'] = df['Value'] * df_area['Value']
|
1245
|
+
|
1246
|
+
if bins[-1] == -1:
|
1247
|
+
if df['Value'].max() > bins[-2]:
|
1248
|
+
bins[-1] = df['Value'].max()
|
1249
|
+
else:
|
1250
|
+
bins[-1] = bins[-2] + (bins[-2] - bins[-3])
|
1251
|
+
|
1252
|
+
if merge_zones:
|
1253
|
+
fig, ax = plt.subplots()
|
1254
|
+
sns.histplot(df, x='Value', hue='Array',
|
1255
|
+
multiple='stack', bins = bins, ax=ax,
|
1256
|
+
weights= df_area['Value'])
|
1257
|
+
|
1258
|
+
# set ticks
|
1259
|
+
ax.set_xticks(bins)
|
1260
|
+
ax.set_xticklabels([f"{b:.2f}" for b in bins])
|
1261
|
+
ax.set_xlabel(self._xlabel)
|
1262
|
+
ax.set_ylabel('Area [m²]')
|
1263
|
+
if show:
|
1264
|
+
plt.show()
|
1265
|
+
|
1266
|
+
return (fig, ax)
|
1267
|
+
else:
|
1268
|
+
# 1 plot per zone
|
1269
|
+
figs, axs = [], []
|
1270
|
+
for i, zone_name in enumerate(self._polygons.keys()):
|
1271
|
+
fig, ax = plt.subplots()
|
1272
|
+
sns.histplot(df[df['Zone'] == zone_name],
|
1273
|
+
x='Value',
|
1274
|
+
hue='Array',
|
1275
|
+
multiple='stack', bins=bins, ax=ax,
|
1276
|
+
weights=df_area[df_area['Zone'] == zone_name]['Value'])
|
1277
|
+
|
1278
|
+
ax.set_xlabel(self._xlabel)
|
1279
|
+
ax.set_ylabel('Area [m²]')
|
1280
|
+
ax.set_title(f'Distribution of Values ({zone_name})')
|
1281
|
+
|
1282
|
+
# set ticks
|
1283
|
+
ax.set_xticks(bins)
|
1284
|
+
ax.set_xticklabels([f"{b:.2f}" for b in bins])
|
1285
|
+
fig.tight_layout()
|
1286
|
+
|
1287
|
+
figs.append(fig)
|
1288
|
+
axs.append(ax)
|
1289
|
+
|
1290
|
+
if show:
|
1291
|
+
plt.show()
|
1292
|
+
|
1293
|
+
return (figs, axs)
|
1294
|
+
|
1295
|
+
class Building_Waterdepth_analysis(Arrays_analysis_zones):
|
1296
|
+
""" Class for water depth analysis of multiple arrays based on a Zones instance.
|
1297
|
+
|
1298
|
+
This class is designed to analyze water depth data from multiple arrays and zones.
|
1299
|
+
It inherits from Arrays_analysis_zones and provides additional methods specific to water depth analysis.
|
1300
|
+
"""
|
1301
|
+
|
1302
|
+
def __init__(self, arrays:dict[str, WolfArray],
|
1303
|
+
zones:Zones | Path | str,
|
1304
|
+
buffer_size:float = 0.0,
|
1305
|
+
merge_zones:bool = False,
|
1306
|
+
thershold_area:float = 0.0):
|
1307
|
+
""" Initialize the class with a list of WolfArray and a Zones instance.
|
1308
|
+
|
1309
|
+
:param arrays: dictionary of WolfArray instances to analyze
|
1310
|
+
:param zones: Zones instance or path to a zones file
|
1311
|
+
:param buffer_size: size of the buffer around the zones (default is 0.0)
|
1312
|
+
:param merge_zones: whether to merge all zones into a single zone (default is False)
|
1313
|
+
:param thershold_area: minimum area of the polygon to consider (default is 0.0)
|
1314
|
+
:raises ValueError: if the arrays are empty or have different shapes
|
1315
|
+
:raises FileNotFoundError: if the zones file does not exist
|
1316
|
+
"""
|
1317
|
+
|
1318
|
+
if isinstance(zones, (Path, str)):
|
1319
|
+
|
1320
|
+
zones = Path(zones)
|
1321
|
+
if not zones.exists():
|
1322
|
+
raise FileNotFoundError(f"Zones file {zones} does not exist.")
|
1323
|
+
|
1324
|
+
[xmin, xmax], [ymin, ymax] = arrays[next(iter(arrays))].get_bounds()
|
1325
|
+
logging.info(f"Using bounds from the first array: {xmin}, {xmax}, {ymin}, {ymax}")
|
1326
|
+
bbox = Polygon([(xmin, ymin), (xmin, ymax), (xmax, ymax), (xmax, ymin)])
|
1327
|
+
logging.info(f"Creating zones from {zones} with bounding box {bbox}")
|
1328
|
+
zones = Zones(zones, bbox=bbox)
|
1329
|
+
logging.info(f"Zones loaded from {zones}")
|
1330
|
+
|
1331
|
+
if merge_zones:
|
1332
|
+
# copy all vectors in an unique zone
|
1333
|
+
newz = Zones(idx='all')
|
1334
|
+
merged_zone = zone(name='all')
|
1335
|
+
newz.add_zone(merged_zone, forceparent= True)
|
1336
|
+
|
1337
|
+
for z in zones.myzones:
|
1338
|
+
if z.used:
|
1339
|
+
merged_zone.myvectors.extend(z.myvectors)
|
1340
|
+
|
1341
|
+
# Rename vectors adding the index position inside the zone
|
1342
|
+
for i, v in enumerate(merged_zone.myvectors):
|
1343
|
+
v.myname = f"{v.myname}___{i}"
|
1344
|
+
|
1345
|
+
zones = newz
|
1346
|
+
|
1347
|
+
for z in zones.myzones:
|
1348
|
+
if z.used:
|
1349
|
+
for v in z.myvectors:
|
1350
|
+
if v.area < thershold_area:
|
1351
|
+
logging.dbg(f"Polygon {v.myname} has an area of {v.area} which is below the threshold of {thershold_area}. It will be ignored.")
|
1352
|
+
v.used = False
|
1353
|
+
|
1354
|
+
super().__init__(arrays, zones, buffer_size)
|
1355
|
+
|
1356
|
+
self._xlabel = _('Water Depth [m]')
|
1357
|
+
|
1358
|
+
def plot_distributed_areas(self, bins = [0, 0.3, 1.3, -1],
|
1359
|
+
operator = 'Median',
|
1360
|
+
show = True,
|
1361
|
+
engine = 'seaborn',
|
1362
|
+
merge_zones = False):
|
199
1363
|
|
1364
|
+
return super()._plot_distributed_areas_seaborn(bins, operator, show=show, merge_zones=merge_zones)
|
200
1365
|
class Slope_analysis:
|
201
1366
|
""" Class for slope analysis of in an array based on a trace vector.
|
202
1367
|
|