ssb-sgis 0.1.5__tar.gz → 0.1.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/PKG-INFO +15 -7
- {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/README.md +14 -6
- {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/pyproject.toml +1 -1
- {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/geopandas_tools/general.py +3 -5
- {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/geopandas_tools/geometry_types.py +1 -1
- {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/geopandas_tools/line_operations.py +1 -1
- {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/maps/explore.py +0 -85
- {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/maps/legend.py +5 -5
- {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/maps/maps.py +5 -1
- {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/maps/thematicmap.py +35 -10
- ssb_sgis-0.1.6/src/sgis/networkanalysis/_get_route.py +244 -0
- {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/networkanalysis/_od_cost_matrix.py +7 -9
- {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/networkanalysis/_points.py +0 -18
- {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/networkanalysis/directednetwork.py +2 -2
- {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/networkanalysis/network.py +16 -25
- {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/networkanalysis/networkanalysis.py +301 -123
- {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/networkanalysis/networkanalysisrules.py +2 -2
- ssb_sgis-0.1.5/src/sgis/networkanalysis/_get_route.py +0 -190
- {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/LICENSE +0 -0
- {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/__init__.py +0 -0
- {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/dapla.py +0 -0
- {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/exceptions.py +0 -0
- {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/geopandas_tools/__init__.py +0 -0
- {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/geopandas_tools/buffer_dissolve_explode.py +0 -0
- {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/geopandas_tools/neighbors.py +0 -0
- {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/geopandas_tools/overlay.py +0 -0
- {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/geopandas_tools/point_operations.py +0 -0
- {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/geopandas_tools/polygon_operations.py +0 -0
- {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/helpers.py +0 -0
- {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/maps/__init__.py +0 -0
- {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/maps/map.py +0 -0
- {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/networkanalysis/__init__.py +0 -0
- {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/networkanalysis/_service_area.py +0 -0
- {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/networkanalysis/network_norway.py +0 -0
- {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/py.typed +0 -0
- {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/read_parquet.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: ssb-sgis
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: GIS functions used at Statistics Norway.
|
|
5
5
|
Home-page: https://github.com/statisticsnorway/ssb-sgis
|
|
6
6
|
License: MIT
|
|
@@ -68,7 +68,7 @@ Preparing for network analysis:
|
|
|
68
68
|
|
|
69
69
|
```python
|
|
70
70
|
import sgis as sg
|
|
71
|
-
|
|
71
|
+
import pandas as pd
|
|
72
72
|
|
|
73
73
|
roads = sg.read_parquet_url(
|
|
74
74
|
"https://media.githubusercontent.com/media/statisticsnorway/ssb-sgis/main/tests/testdata/roads_oslo_2022.parquet"
|
|
@@ -97,14 +97,23 @@ nwa
|
|
|
97
97
|
log=True, detailed_log=True,
|
|
98
98
|
)
|
|
99
99
|
|
|
100
|
-
Get number of times each line segment was visited.
|
|
100
|
+
Get number of times each line segment was visited, with optional weighting.
|
|
101
101
|
|
|
102
102
|
```python
|
|
103
|
-
|
|
103
|
+
origins = points.iloc[:75]
|
|
104
|
+
destinations = points.iloc[75:150]
|
|
105
|
+
|
|
106
|
+
# creating uniform weights of 10
|
|
107
|
+
od_pairs = pd.MultiIndex.from_product([origins.index, destinations.index])
|
|
108
|
+
weights = pd.DataFrame(index=od_pairs)
|
|
109
|
+
weights["weight"] = 10
|
|
104
110
|
|
|
111
|
+
frequencies = nwa.get_route_frequencies(origins, destinations, weight_df=weights)
|
|
112
|
+
|
|
113
|
+
# plot the results
|
|
105
114
|
m = sg.ThematicMap(sg.buff(frequencies, 15), column="frequency", black=True)
|
|
106
115
|
m.cmap = "plasma"
|
|
107
|
-
m.title = "Number of times each road was used
|
|
116
|
+
m.title = "Number of times each road was used,\nweighted * 10"
|
|
108
117
|
m.plot()
|
|
109
118
|
```
|
|
110
119
|
|
|
@@ -141,6 +150,7 @@ service_areas = nwa.service_area(
|
|
|
141
150
|
breaks=np.arange(1, 11),
|
|
142
151
|
)
|
|
143
152
|
|
|
153
|
+
# plot the results
|
|
144
154
|
m = sg.ThematicMap(service_areas, column="minutes", black=True, size=10)
|
|
145
155
|
m.k = 10
|
|
146
156
|
m.title = "Roads that can be reached within 1 to 10 minutes"
|
|
@@ -164,8 +174,6 @@ m.plot()
|
|
|
164
174
|
|
|
165
175
|

|
|
166
176
|
|
|
167
|
-
More network analysis examples can be found here: https://github.com/statisticsnorway/ssb-sgis/blob/main/docs/network_analysis_demo_template.md
|
|
168
|
-
|
|
169
177
|
Road data for Norway can be downloaded here: https://kartkatalog.geonorge.no/metadata/nvdb-ruteplan-nettverksdatasett/8d0f9066-34f9-4423-be12-8e8523089313
|
|
170
178
|
|
|
171
179
|
## Developer information
|
|
@@ -34,7 +34,7 @@ Preparing for network analysis:
|
|
|
34
34
|
|
|
35
35
|
```python
|
|
36
36
|
import sgis as sg
|
|
37
|
-
|
|
37
|
+
import pandas as pd
|
|
38
38
|
|
|
39
39
|
roads = sg.read_parquet_url(
|
|
40
40
|
"https://media.githubusercontent.com/media/statisticsnorway/ssb-sgis/main/tests/testdata/roads_oslo_2022.parquet"
|
|
@@ -63,14 +63,23 @@ nwa
|
|
|
63
63
|
log=True, detailed_log=True,
|
|
64
64
|
)
|
|
65
65
|
|
|
66
|
-
Get number of times each line segment was visited.
|
|
66
|
+
Get number of times each line segment was visited, with optional weighting.
|
|
67
67
|
|
|
68
68
|
```python
|
|
69
|
-
|
|
69
|
+
origins = points.iloc[:75]
|
|
70
|
+
destinations = points.iloc[75:150]
|
|
71
|
+
|
|
72
|
+
# creating uniform weights of 10
|
|
73
|
+
od_pairs = pd.MultiIndex.from_product([origins.index, destinations.index])
|
|
74
|
+
weights = pd.DataFrame(index=od_pairs)
|
|
75
|
+
weights["weight"] = 10
|
|
70
76
|
|
|
77
|
+
frequencies = nwa.get_route_frequencies(origins, destinations, weight_df=weights)
|
|
78
|
+
|
|
79
|
+
# plot the results
|
|
71
80
|
m = sg.ThematicMap(sg.buff(frequencies, 15), column="frequency", black=True)
|
|
72
81
|
m.cmap = "plasma"
|
|
73
|
-
m.title = "Number of times each road was used
|
|
82
|
+
m.title = "Number of times each road was used,\nweighted * 10"
|
|
74
83
|
m.plot()
|
|
75
84
|
```
|
|
76
85
|
|
|
@@ -107,6 +116,7 @@ service_areas = nwa.service_area(
|
|
|
107
116
|
breaks=np.arange(1, 11),
|
|
108
117
|
)
|
|
109
118
|
|
|
119
|
+
# plot the results
|
|
110
120
|
m = sg.ThematicMap(service_areas, column="minutes", black=True, size=10)
|
|
111
121
|
m.k = 10
|
|
112
122
|
m.title = "Roads that can be reached within 1 to 10 minutes"
|
|
@@ -130,8 +140,6 @@ m.plot()
|
|
|
130
140
|
|
|
131
141
|

|
|
132
142
|
|
|
133
|
-
More network analysis examples can be found here: https://github.com/statisticsnorway/ssb-sgis/blob/main/docs/network_analysis_demo_template.md
|
|
134
|
-
|
|
135
143
|
Road data for Norway can be downloaded here: https://kartkatalog.geonorge.no/metadata/nvdb-ruteplan-nettverksdatasett/8d0f9066-34f9-4423-be12-8e8523089313
|
|
136
144
|
|
|
137
145
|
## Developer information
|
|
@@ -368,8 +368,8 @@ def to_lines(*gdfs: GeoDataFrame, copy: bool = True) -> GeoDataFrame:
|
|
|
368
368
|
>>> poly1["poly1"] = 1
|
|
369
369
|
>>> line = sg.to_lines(poly1)
|
|
370
370
|
>>> line
|
|
371
|
-
|
|
372
|
-
0
|
|
371
|
+
geometry poly1
|
|
372
|
+
0 LINESTRING (0.00000 0.00000, 0.00000 1.00000, ... 1
|
|
373
373
|
|
|
374
374
|
Convert two overlapping polygons to linestrings.
|
|
375
375
|
|
|
@@ -385,13 +385,11 @@ def to_lines(*gdfs: GeoDataFrame, copy: bool = True) -> GeoDataFrame:
|
|
|
385
385
|
4 NaN 1.0 LINESTRING (0.50000 1.00000, 0.50000 1.50000, ...
|
|
386
386
|
5 NaN 1.0 LINESTRING (1.00000 0.50000, 0.50000 0.50000)
|
|
387
387
|
|
|
388
|
-
Plot before and after
|
|
388
|
+
Plot before and after.
|
|
389
389
|
|
|
390
390
|
>>> sg.qtm(poly1, poly2)
|
|
391
|
-
<Axes: >
|
|
392
391
|
>>> lines["l"] = lines.length
|
|
393
392
|
>>> sg.qtm(lines, "l")
|
|
394
|
-
<Axes: >
|
|
395
393
|
"""
|
|
396
394
|
|
|
397
395
|
if any(any(gdf.geom_type.isin(["Point", "MultiPoint"])) for gdf in gdfs):
|
|
@@ -698,7 +698,7 @@ def close_network_holes_to_deadends(
|
|
|
698
698
|
|
|
699
699
|
Fill gaps shorter than 1.1 meters.
|
|
700
700
|
|
|
701
|
-
>>> filled = sg.close_network_holes_to_deadends(roads, max_distance=1.1
|
|
701
|
+
>>> filled = sg.close_network_holes_to_deadends(roads, max_distance=1.1)
|
|
702
702
|
>>> roads = sg.get_largest_component(roads)
|
|
703
703
|
>>> roads.connected.value_counts()
|
|
704
704
|
1.0 100315
|
|
@@ -85,42 +85,6 @@ class Explore(Map):
|
|
|
85
85
|
self.cmap_stop = kwargs.pop("cmap_stop", 256)
|
|
86
86
|
|
|
87
87
|
def explore(self, column: str | None = None, **kwargs) -> None:
|
|
88
|
-
"""Interactive map of the GeoDataFrames with layers that can be toggles on/off.
|
|
89
|
-
|
|
90
|
-
It displays all the GeoDataFrames and displays them together in an interactive
|
|
91
|
-
map with a common legend. The layers can be toggled on and off.
|
|
92
|
-
|
|
93
|
-
Args:
|
|
94
|
-
column: The column to color the geometries by. Defaults to the column
|
|
95
|
-
that was specified last.
|
|
96
|
-
**kwargs: Keyword arguments to pass to geopandas.GeoDataFrame.explore, for
|
|
97
|
-
instance 'cmap' to change the colors, 'scheme' to change how the data
|
|
98
|
-
is grouped. This defaults to 'fisherjenks' for numeric data.
|
|
99
|
-
|
|
100
|
-
See also:
|
|
101
|
-
samplemap: same functionality, but shows only a random area of a given size.
|
|
102
|
-
clipmap: same functionality, but shows only the areas clipped by a given
|
|
103
|
-
mask.
|
|
104
|
-
|
|
105
|
-
Examples
|
|
106
|
-
--------
|
|
107
|
-
>>> from sgis import read_parquet_url
|
|
108
|
-
>>> roads = read_parquet_url("https://media.githubusercontent.com/media/statisticsnorway/ssb-sgis/main/tests/testdata/roads_oslo_2022.parquet")
|
|
109
|
-
>>> points = read_parquet_url("https://media.githubusercontent.com/media/statisticsnorway/ssb-sgis/main/tests/testdata/points_oslo.parquet")
|
|
110
|
-
|
|
111
|
-
Simple explore of two GeoDataFrames.
|
|
112
|
-
|
|
113
|
-
>>> from sgis import Explore
|
|
114
|
-
>>> ex = Explore(roads, points)
|
|
115
|
-
>>> ex.explore()
|
|
116
|
-
|
|
117
|
-
With column.
|
|
118
|
-
|
|
119
|
-
>>> roads["meters"] = roads.length
|
|
120
|
-
>>> points["meters"] = points.length
|
|
121
|
-
>>> ex = Explore(roads, points, column="meters")
|
|
122
|
-
>>> ex.samplemap()
|
|
123
|
-
"""
|
|
124
88
|
if column:
|
|
125
89
|
self._column = column
|
|
126
90
|
self._update_column()
|
|
@@ -135,34 +99,6 @@ class Explore(Map):
|
|
|
135
99
|
sample_from_first: bool = True,
|
|
136
100
|
**kwargs,
|
|
137
101
|
) -> None:
|
|
138
|
-
"""Shows an interactive map of a random area of the GeoDataFrames.
|
|
139
|
-
|
|
140
|
-
It takes a random sample point of the GeoDataFrames, and shows all geometries
|
|
141
|
-
within a given radius of this point. Displays an interactive map with a common
|
|
142
|
-
legend. The layers can be toggled on and off.
|
|
143
|
-
|
|
144
|
-
The radius to plot can be changed with the 'size' parameter.
|
|
145
|
-
|
|
146
|
-
For more info about the labeling and coloring of the map, see the explore
|
|
147
|
-
method.
|
|
148
|
-
|
|
149
|
-
Args:
|
|
150
|
-
size: the radius to buffer the sample point by before clipping with the
|
|
151
|
-
data.
|
|
152
|
-
column: The column to color the geometries by. Defaults to the column
|
|
153
|
-
that was specified last.
|
|
154
|
-
sample_from_first: If True (default), the sample point is taken from
|
|
155
|
-
the first specified GeoDataFrame. If False, all GeoDataFrames are
|
|
156
|
-
considered.
|
|
157
|
-
**kwargs: Keyword arguments to pass to geopandas.GeoDataFrame.explore, for
|
|
158
|
-
instance 'cmap' to change the colors, 'scheme' to change how the data
|
|
159
|
-
is grouped. This defaults to 'fisherjenks' for numeric data.
|
|
160
|
-
|
|
161
|
-
See also:
|
|
162
|
-
explore: same functionality, but shows the entire area of the geometries.
|
|
163
|
-
clipmap: same functionality, but shows only the areas clipped by a given
|
|
164
|
-
mask.
|
|
165
|
-
"""
|
|
166
102
|
if column:
|
|
167
103
|
self._column = column
|
|
168
104
|
self._update_column()
|
|
@@ -200,27 +136,6 @@ class Explore(Map):
|
|
|
200
136
|
column: str | None = None,
|
|
201
137
|
**kwargs,
|
|
202
138
|
) -> None:
|
|
203
|
-
"""Shows an interactive map of a of the GeoDataFrames clipped by the mask.
|
|
204
|
-
|
|
205
|
-
It clips all the GeoDataFrames in the Explore instance to the mask extent,
|
|
206
|
-
and displays the resulting geometries in an interactive map with a common
|
|
207
|
-
legends. The layers can be toggled on and off.
|
|
208
|
-
|
|
209
|
-
For more info about the labeling and coloring of the map, see the explore
|
|
210
|
-
method.
|
|
211
|
-
|
|
212
|
-
Args:
|
|
213
|
-
mask: the geometry to clip the data by.
|
|
214
|
-
column: The column to color the geometries by. Defaults to the column
|
|
215
|
-
that was specified last.
|
|
216
|
-
**kwargs: Keyword arguments to pass to geopandas.GeoDataFrame.explore, for
|
|
217
|
-
instance 'cmap' to change the colors, 'scheme' to change how the data
|
|
218
|
-
is grouped. This defaults to 'fisherjenks' for numeric data.
|
|
219
|
-
|
|
220
|
-
See also:
|
|
221
|
-
explore: same functionality, but shows the entire area of the geometries.
|
|
222
|
-
samplemap: same functionality, but shows only a random area of a given size.
|
|
223
|
-
"""
|
|
224
139
|
if column:
|
|
225
140
|
self._column = column
|
|
226
141
|
self._update_column()
|
|
@@ -157,9 +157,7 @@ class Legend:
|
|
|
157
157
|
else:
|
|
158
158
|
self._markersize = size
|
|
159
159
|
|
|
160
|
-
def
|
|
161
|
-
self, ax, categories_colors: dict, nan_label: str
|
|
162
|
-
):
|
|
160
|
+
def _prepare_categorical_legend(self, categories_colors: dict, nan_label: str):
|
|
163
161
|
for attr in self.__dict__.keys():
|
|
164
162
|
if attr in self.kwargs:
|
|
165
163
|
self[attr] = self.kwargs.pop(attr)
|
|
@@ -196,6 +194,8 @@ class Legend:
|
|
|
196
194
|
markeredgewidth=0,
|
|
197
195
|
)
|
|
198
196
|
)
|
|
197
|
+
|
|
198
|
+
def _actually_add_legend(self, ax):
|
|
199
199
|
legend = ax.legend(
|
|
200
200
|
self._patches,
|
|
201
201
|
self._categories,
|
|
@@ -433,9 +433,8 @@ class ContinousLegend(Legend):
|
|
|
433
433
|
if not self._legend:
|
|
434
434
|
raise ValueError("Cannot modify legend before it is created.")
|
|
435
435
|
|
|
436
|
-
def
|
|
436
|
+
def _prepare_continous_legend(
|
|
437
437
|
self,
|
|
438
|
-
ax,
|
|
439
438
|
bins: list[float],
|
|
440
439
|
colors: list[str],
|
|
441
440
|
nan_label: str,
|
|
@@ -524,6 +523,7 @@ class ContinousLegend(Legend):
|
|
|
524
523
|
label = self._two_value_label(min_rounded, max_rounded)
|
|
525
524
|
self._categories.append(label)
|
|
526
525
|
|
|
526
|
+
def _actually_add_legend(self, ax):
|
|
527
527
|
legend = ax.legend(
|
|
528
528
|
self._patches,
|
|
529
529
|
self._categories,
|
|
@@ -37,12 +37,16 @@ def explore(
|
|
|
37
37
|
show_in_browser: bool = False,
|
|
38
38
|
**kwargs,
|
|
39
39
|
) -> None:
|
|
40
|
-
"""Interactive map of GeoDataFrames with layers that can be
|
|
40
|
+
"""Interactive map of GeoDataFrames with layers that can be toggled on/off.
|
|
41
41
|
|
|
42
42
|
It takes all the given GeoDataFrames and displays them together in an
|
|
43
43
|
interactive map with a common legend. If 'column' is not specified, each
|
|
44
44
|
GeoDataFrame is given a unique color.
|
|
45
45
|
|
|
46
|
+
If the column is of type string and only one GeoDataFrame is given, the unique
|
|
47
|
+
values will be split into separate GeoDataFrames so that each value can be toggled
|
|
48
|
+
on/off.
|
|
49
|
+
|
|
46
50
|
The coloring can be changed with the 'cmap' parameter. The default colormap is a
|
|
47
51
|
custom, strongly colored palette. If a numerical colum is given, the 'viridis'
|
|
48
52
|
palette is used.
|
|
@@ -146,26 +146,56 @@ class ThematicMap(Map):
|
|
|
146
146
|
This method should be run after customising the map, but before saving.
|
|
147
147
|
"""
|
|
148
148
|
|
|
149
|
+
__test = kwargs.pop("__test", False)
|
|
149
150
|
include_legend = bool(kwargs.pop("legend", self.legend))
|
|
150
151
|
|
|
151
|
-
self._prepare_plot(**kwargs)
|
|
152
|
-
|
|
153
152
|
if "color" in kwargs:
|
|
154
|
-
kwargs
|
|
153
|
+
kwargs.pop("column", None)
|
|
155
154
|
self.legend = None
|
|
156
155
|
include_legend = False
|
|
157
156
|
elif hasattr(self, "color"):
|
|
158
|
-
kwargs
|
|
157
|
+
kwargs.pop("column", None)
|
|
159
158
|
kwargs["color"] = self.color
|
|
160
159
|
self.legend = None
|
|
161
160
|
include_legend = False
|
|
161
|
+
|
|
162
162
|
elif self._is_categorical:
|
|
163
163
|
kwargs = self._prepare_categorical_plot(kwargs)
|
|
164
|
+
self.legend._prepare_categorical_legend(
|
|
165
|
+
categories_colors=self._categories_colors_dict,
|
|
166
|
+
nan_label=self.nan_label,
|
|
167
|
+
)
|
|
168
|
+
|
|
164
169
|
else:
|
|
165
170
|
kwargs = self._prepare_continous_plot(kwargs)
|
|
171
|
+
if self.legend:
|
|
172
|
+
if not self.legend._rounding_has_been_set:
|
|
173
|
+
self.legend._rounding = self.legend._get_rounding(
|
|
174
|
+
array=self._gdf.loc[~self._nan_idx, self._column]
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
self.legend._prepare_continous_legend(
|
|
178
|
+
bins=self.bins,
|
|
179
|
+
colors=self._unique_colors,
|
|
180
|
+
nan_label=self.nan_label,
|
|
181
|
+
bin_values=self._bins_unique_values,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
if self.legend and not self.legend._position_has_been_set:
|
|
185
|
+
self.legend._position = self.legend._get_best_legend_position(
|
|
186
|
+
self._gdf, k=self._k + bool(len(self._nan_idx))
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
if __test:
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
self._prepare_plot(**kwargs)
|
|
166
193
|
|
|
167
194
|
if self.legend:
|
|
168
|
-
self._actually_add_legend()
|
|
195
|
+
self.ax = self.legend._actually_add_legend(ax=self.ax)
|
|
196
|
+
|
|
197
|
+
# if self.legend:
|
|
198
|
+
# self._actually_add_legend()
|
|
169
199
|
|
|
170
200
|
self._gdf.plot(legend=include_legend, ax=self.ax, **kwargs)
|
|
171
201
|
|
|
@@ -257,11 +287,6 @@ class ThematicMap(Map):
|
|
|
257
287
|
self._gdf, k=self._k + bool(len(self._nan_idx))
|
|
258
288
|
)
|
|
259
289
|
|
|
260
|
-
if not self._is_categorical and not self.legend._rounding_has_been_set:
|
|
261
|
-
self.legend._rounding = self.legend._get_rounding(
|
|
262
|
-
array=self._gdf.loc[~self._nan_idx, self._column]
|
|
263
|
-
)
|
|
264
|
-
|
|
265
290
|
if self._is_categorical:
|
|
266
291
|
self.ax = self.legend._actually_add_categorical_legend(
|
|
267
292
|
ax=self.ax,
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from geopandas import GeoDataFrame
|
|
6
|
+
from igraph import Graph
|
|
7
|
+
from pandas import DataFrame
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _get_route(
|
|
11
|
+
graph: Graph,
|
|
12
|
+
origins: GeoDataFrame,
|
|
13
|
+
destinations: GeoDataFrame,
|
|
14
|
+
weight: str,
|
|
15
|
+
roads: GeoDataFrame,
|
|
16
|
+
rowwise: bool = False,
|
|
17
|
+
) -> GeoDataFrame:
|
|
18
|
+
"""Function used in the get_route method of NetworkAnalysis."""
|
|
19
|
+
|
|
20
|
+
warnings.filterwarnings("ignore", category=RuntimeWarning)
|
|
21
|
+
|
|
22
|
+
od_pairs = _create_od_pairs(origins, destinations, rowwise)
|
|
23
|
+
|
|
24
|
+
resultlist: list[DataFrame] = []
|
|
25
|
+
|
|
26
|
+
for ori_id, des_id in od_pairs:
|
|
27
|
+
indices = _get_one_route(graph, ori_id, des_id)
|
|
28
|
+
|
|
29
|
+
if not indices:
|
|
30
|
+
continue
|
|
31
|
+
|
|
32
|
+
line_ids = _create_line_id_df(indices["source_target_weight"], ori_id, des_id)
|
|
33
|
+
|
|
34
|
+
resultlist.append(line_ids)
|
|
35
|
+
|
|
36
|
+
if not resultlist:
|
|
37
|
+
warnings.warn(
|
|
38
|
+
"No paths were found. Try larger search_tolerance or search_factor. "
|
|
39
|
+
"Or close_network_holes() or remove_isolated()."
|
|
40
|
+
)
|
|
41
|
+
return pd.DataFrame(columns=["origin", "destination", weight, "geometry"])
|
|
42
|
+
|
|
43
|
+
results: DataFrame = pd.concat(resultlist)
|
|
44
|
+
assert list(results.columns) == ["origin", "destination"], list(results.columns)
|
|
45
|
+
lines: GeoDataFrame = _get_line_geometries(results, roads, weight)
|
|
46
|
+
lines = lines.dissolve(by=["origin", "destination"], aggfunc="sum", as_index=False)
|
|
47
|
+
|
|
48
|
+
return lines[["origin", "destination", weight, "geometry"]]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _get_k_routes(
|
|
52
|
+
graph: Graph,
|
|
53
|
+
origins: GeoDataFrame,
|
|
54
|
+
destinations: GeoDataFrame,
|
|
55
|
+
weight: str,
|
|
56
|
+
roads: GeoDataFrame,
|
|
57
|
+
k: int,
|
|
58
|
+
drop_middle_percent: int,
|
|
59
|
+
rowwise: bool,
|
|
60
|
+
) -> GeoDataFrame:
|
|
61
|
+
"""Function used in the get_k_routes method of NetworkAnalysis."""
|
|
62
|
+
warnings.filterwarnings("ignore", category=RuntimeWarning)
|
|
63
|
+
od_pairs = _create_od_pairs(origins, destinations, rowwise)
|
|
64
|
+
|
|
65
|
+
resultlist: list[DataFrame] = []
|
|
66
|
+
|
|
67
|
+
for ori_id, des_id in od_pairs:
|
|
68
|
+
k_lines: DataFrame = _loop_k_routes(
|
|
69
|
+
graph, ori_id, des_id, k, drop_middle_percent
|
|
70
|
+
)
|
|
71
|
+
if k_lines is not None:
|
|
72
|
+
resultlist.append(k_lines)
|
|
73
|
+
|
|
74
|
+
if not resultlist:
|
|
75
|
+
warnings.warn(
|
|
76
|
+
"No paths were found. Try larger search_tolerance or search_factor. "
|
|
77
|
+
"Or close_network_holes() or remove_isolated()."
|
|
78
|
+
)
|
|
79
|
+
return pd.DataFrame(columns=["origin", "destination", weight, "geometry"])
|
|
80
|
+
|
|
81
|
+
results: DataFrame = pd.concat(resultlist)
|
|
82
|
+
assert list(results.columns) == ["origin", "destination", "k"], list(
|
|
83
|
+
results.columns
|
|
84
|
+
)
|
|
85
|
+
lines: GeoDataFrame = _get_line_geometries(results, roads, weight)
|
|
86
|
+
|
|
87
|
+
lines = lines.dissolve(
|
|
88
|
+
by=["origin", "destination", "k"], aggfunc="sum", as_index=False
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
return lines[["origin", "destination", weight, "k", "geometry"]]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _get_route_frequencies(
|
|
95
|
+
graph,
|
|
96
|
+
origins,
|
|
97
|
+
destinations,
|
|
98
|
+
rowwise,
|
|
99
|
+
roads,
|
|
100
|
+
weight_df: DataFrame | None = None,
|
|
101
|
+
):
|
|
102
|
+
"""Function used in the get_route_frequencies method of NetworkAnalysis."""
|
|
103
|
+
warnings.filterwarnings("ignore", category=RuntimeWarning)
|
|
104
|
+
od_pairs = _create_od_pairs(origins, destinations, rowwise)
|
|
105
|
+
|
|
106
|
+
if weight_df is not None and len(weight_df) != len(od_pairs):
|
|
107
|
+
error_message = _make_keyerror_message(rowwise, weight_df, origins)
|
|
108
|
+
raise ValueError(error_message)
|
|
109
|
+
|
|
110
|
+
resultlist: list[DataFrame] = []
|
|
111
|
+
|
|
112
|
+
for ori_id, des_id in od_pairs:
|
|
113
|
+
indices = _get_one_route(graph, ori_id, des_id)
|
|
114
|
+
|
|
115
|
+
if not indices:
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
line_ids = DataFrame({"source_target_weight": indices["source_target_weight"]})
|
|
119
|
+
line_ids["origin"] = ori_id
|
|
120
|
+
line_ids["destination"] = des_id
|
|
121
|
+
|
|
122
|
+
if weight_df is not None:
|
|
123
|
+
try:
|
|
124
|
+
line_ids["multiplier"] = weight_df.loc[ori_id, des_id].iloc[0]
|
|
125
|
+
except KeyError as e:
|
|
126
|
+
error_message = _make_keyerror_message(rowwise, weight_df, origins)
|
|
127
|
+
raise KeyError(error_message) from e
|
|
128
|
+
else:
|
|
129
|
+
line_ids["multiplier"] = 1
|
|
130
|
+
|
|
131
|
+
resultlist.append(line_ids)
|
|
132
|
+
|
|
133
|
+
summarised = (
|
|
134
|
+
pd.concat(resultlist, ignore_index=True)
|
|
135
|
+
.groupby("source_target_weight")["multiplier"]
|
|
136
|
+
.sum()
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
roads["frequency"] = roads["source_target_weight"].map(summarised)
|
|
140
|
+
|
|
141
|
+
roads_visited = roads.loc[
|
|
142
|
+
roads.frequency.notna(), roads.columns.difference(["source_target_weight"])
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
return roads_visited
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _create_od_pairs(
|
|
149
|
+
origins: GeoDataFrame, destinations: GeoDataFrame, rowwise: bool
|
|
150
|
+
) -> zip | pd.MultiIndex:
|
|
151
|
+
"""Get all od combinaions if not rowwise."""
|
|
152
|
+
if rowwise:
|
|
153
|
+
return zip(origins.temp_idx, destinations.temp_idx)
|
|
154
|
+
else:
|
|
155
|
+
return pd.MultiIndex.from_product([origins.temp_idx, destinations.temp_idx])
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _get_one_route(graph: Graph, ori_id: str, des_id: str):
|
|
159
|
+
"""Get the edges for one route."""
|
|
160
|
+
res = graph.get_shortest_paths(
|
|
161
|
+
weights="weight", v=ori_id, to=des_id, output="epath"
|
|
162
|
+
)
|
|
163
|
+
if not res[0]:
|
|
164
|
+
return []
|
|
165
|
+
|
|
166
|
+
return graph.es[res[0]]
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _get_line_geometries(line_ids, roads, weight) -> GeoDataFrame:
|
|
170
|
+
road_mapper = roads.set_index(["source_target_weight"])[[weight, "geometry"]]
|
|
171
|
+
line_ids = line_ids.join(road_mapper)
|
|
172
|
+
return GeoDataFrame(line_ids, geometry="geometry", crs=roads.crs)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _create_line_id_df(source_target_weight: list, ori_id, des_id) -> DataFrame:
|
|
176
|
+
line_ids = DataFrame(index=source_target_weight)
|
|
177
|
+
|
|
178
|
+
# remove edges from ori/des to the roads
|
|
179
|
+
line_ids = line_ids.loc[~line_ids.index.str.endswith("_0")]
|
|
180
|
+
|
|
181
|
+
line_ids["origin"] = ori_id
|
|
182
|
+
line_ids["destination"] = des_id
|
|
183
|
+
|
|
184
|
+
return line_ids
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _loop_k_routes(graph: Graph, ori_id, des_id, k, drop_middle_percent) -> DataFrame:
|
|
188
|
+
"""Workaround for igraph's get_k_shortest_paths.
|
|
189
|
+
|
|
190
|
+
igraph's get_k_shorest_paths doesn't seem to work (gives just the same path k
|
|
191
|
+
times), so doing it manually. Run _get_one_route, then remove the edges in the
|
|
192
|
+
middle of the route, given with drop_middle_percent, repeat k times.
|
|
193
|
+
"""
|
|
194
|
+
graph = graph.copy()
|
|
195
|
+
|
|
196
|
+
lines: list[DataFrame] = []
|
|
197
|
+
|
|
198
|
+
for i in range(k):
|
|
199
|
+
indices = _get_one_route(graph, ori_id, des_id)
|
|
200
|
+
|
|
201
|
+
if not indices:
|
|
202
|
+
continue
|
|
203
|
+
|
|
204
|
+
line_ids = _create_line_id_df(indices["source_target_weight"], ori_id, des_id)
|
|
205
|
+
line_ids["k"] = i + 1
|
|
206
|
+
lines.append(line_ids)
|
|
207
|
+
|
|
208
|
+
edge_tuples = indices["edge_tuples"]
|
|
209
|
+
|
|
210
|
+
n_edges_to_keep = (
|
|
211
|
+
len(edge_tuples) - len(edge_tuples) * drop_middle_percent / 100
|
|
212
|
+
) / 2
|
|
213
|
+
|
|
214
|
+
n_edges_to_keep = int(round(n_edges_to_keep, 0))
|
|
215
|
+
|
|
216
|
+
if n_edges_to_keep == 0:
|
|
217
|
+
n_edges_to_keep = 1
|
|
218
|
+
|
|
219
|
+
to_be_dropped = edge_tuples[n_edges_to_keep:-n_edges_to_keep]
|
|
220
|
+
graph.delete_edges(to_be_dropped)
|
|
221
|
+
|
|
222
|
+
if lines:
|
|
223
|
+
return pd.concat(lines)
|
|
224
|
+
else:
|
|
225
|
+
return pd.DataFrame()
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _make_keyerror_message(rowwise, weight_df, origins) -> str:
|
|
229
|
+
"""Add help info to error message if key in weight_df is missing.
|
|
230
|
+
|
|
231
|
+
If empty resultlist, assume all indices are wrong. Else, assume
|
|
232
|
+
"""
|
|
233
|
+
error_message = (
|
|
234
|
+
"'weight_df' does not contain all indices of each OD pair combination. "
|
|
235
|
+
)
|
|
236
|
+
if not rowwise and len(weight_df) == len(origins):
|
|
237
|
+
error_message = error_message + (
|
|
238
|
+
"Did you mean to set rowwise to True? "
|
|
239
|
+
"If not, make sure weight_df contains all combinations of "
|
|
240
|
+
"origin-destination pairs. Either specified as a MultiIndex or as the "
|
|
241
|
+
"first two columns of 'weight_df'. So (0, 0), (0, 1), (1, 0), (1, 1) etc."
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
return error_message
|