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.
Files changed (36) hide show
  1. {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/PKG-INFO +15 -7
  2. {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/README.md +14 -6
  3. {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/pyproject.toml +1 -1
  4. {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/geopandas_tools/general.py +3 -5
  5. {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/geopandas_tools/geometry_types.py +1 -1
  6. {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/geopandas_tools/line_operations.py +1 -1
  7. {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/maps/explore.py +0 -85
  8. {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/maps/legend.py +5 -5
  9. {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/maps/maps.py +5 -1
  10. {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/maps/thematicmap.py +35 -10
  11. ssb_sgis-0.1.6/src/sgis/networkanalysis/_get_route.py +244 -0
  12. {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/networkanalysis/_od_cost_matrix.py +7 -9
  13. {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/networkanalysis/_points.py +0 -18
  14. {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/networkanalysis/directednetwork.py +2 -2
  15. {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/networkanalysis/network.py +16 -25
  16. {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/networkanalysis/networkanalysis.py +301 -123
  17. {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/networkanalysis/networkanalysisrules.py +2 -2
  18. ssb_sgis-0.1.5/src/sgis/networkanalysis/_get_route.py +0 -190
  19. {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/LICENSE +0 -0
  20. {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/__init__.py +0 -0
  21. {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/dapla.py +0 -0
  22. {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/exceptions.py +0 -0
  23. {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/geopandas_tools/__init__.py +0 -0
  24. {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/geopandas_tools/buffer_dissolve_explode.py +0 -0
  25. {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/geopandas_tools/neighbors.py +0 -0
  26. {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/geopandas_tools/overlay.py +0 -0
  27. {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/geopandas_tools/point_operations.py +0 -0
  28. {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/geopandas_tools/polygon_operations.py +0 -0
  29. {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/helpers.py +0 -0
  30. {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/maps/__init__.py +0 -0
  31. {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/maps/map.py +0 -0
  32. {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/networkanalysis/__init__.py +0 -0
  33. {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/networkanalysis/_service_area.py +0 -0
  34. {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/networkanalysis/network_norway.py +0 -0
  35. {ssb_sgis-0.1.5 → ssb_sgis-0.1.6}/src/sgis/py.typed +0 -0
  36. {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.5
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
- frequencies = nwa.get_route_frequencies(points.sample(75), points.sample(75))
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
  ![png](docs/examples/network_analysis_examples_files/network_analysis_examples_11_0.png)
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
- frequencies = nwa.get_route_frequencies(points.sample(75), points.sample(75))
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
  ![png](docs/examples/network_analysis_examples_files/network_analysis_examples_11_0.png)
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
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "ssb-sgis"
3
- version = "0.1.5"
3
+ version = "0.1.6"
4
4
  description = "GIS functions used at Statistics Norway."
5
5
  authors = ["Statistics Norway <ort@ssb.no>"]
6
6
  license = "MIT"
@@ -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
- poly1 geometry
372
- 0 1 LINESTRING (0.00000 0.00000, 0.00000 1.00000, ...
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 (plots not showing in terminal).
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):
@@ -60,7 +60,7 @@ def to_single_geom_type(
60
60
  0 GEOMETRYCOLLECTION (POINT (0.00000 0.00000), L...
61
61
 
62
62
  >>> to_single_geom_type(gdf, "line")
63
- geometry
63
+ geometry
64
64
  2 LINESTRING (1.00000 1.00000, 2.00000 2.00000)
65
65
  """
66
66
  if not isinstance(gdf, (GeoDataFrame, GeoSeries)):
@@ -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, max_angle=30)
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 _actually_add_categorical_legend(
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 _actually_add_continous_legend(
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 toggles on/off.
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["column"] = self.column
153
+ kwargs.pop("column", None)
155
154
  self.legend = None
156
155
  include_legend = False
157
156
  elif hasattr(self, "color"):
158
- kwargs["column"] = self.column
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