ssb-sgis 1.0.2__py3-none-any.whl → 1.0.4__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.
Files changed (50) hide show
  1. sgis/__init__.py +20 -9
  2. sgis/debug_config.py +24 -0
  3. sgis/exceptions.py +2 -2
  4. sgis/geopandas_tools/bounds.py +33 -36
  5. sgis/geopandas_tools/buffer_dissolve_explode.py +136 -35
  6. sgis/geopandas_tools/centerlines.py +4 -91
  7. sgis/geopandas_tools/cleaning.py +1576 -583
  8. sgis/geopandas_tools/conversion.py +38 -19
  9. sgis/geopandas_tools/duplicates.py +29 -8
  10. sgis/geopandas_tools/general.py +263 -100
  11. sgis/geopandas_tools/geometry_types.py +4 -4
  12. sgis/geopandas_tools/neighbors.py +19 -15
  13. sgis/geopandas_tools/overlay.py +2 -2
  14. sgis/geopandas_tools/point_operations.py +5 -5
  15. sgis/geopandas_tools/polygon_operations.py +510 -105
  16. sgis/geopandas_tools/polygons_as_rings.py +40 -8
  17. sgis/geopandas_tools/sfilter.py +29 -12
  18. sgis/helpers.py +3 -3
  19. sgis/io/dapla_functions.py +238 -19
  20. sgis/io/read_parquet.py +1 -1
  21. sgis/maps/examine.py +27 -12
  22. sgis/maps/explore.py +450 -65
  23. sgis/maps/legend.py +177 -76
  24. sgis/maps/map.py +206 -103
  25. sgis/maps/maps.py +178 -105
  26. sgis/maps/thematicmap.py +243 -83
  27. sgis/networkanalysis/_service_area.py +6 -1
  28. sgis/networkanalysis/closing_network_holes.py +2 -2
  29. sgis/networkanalysis/cutting_lines.py +15 -8
  30. sgis/networkanalysis/directednetwork.py +1 -1
  31. sgis/networkanalysis/finding_isolated_networks.py +15 -8
  32. sgis/networkanalysis/networkanalysis.py +17 -19
  33. sgis/networkanalysis/networkanalysisrules.py +1 -1
  34. sgis/networkanalysis/traveling_salesman.py +1 -1
  35. sgis/parallel/parallel.py +64 -27
  36. sgis/raster/__init__.py +0 -6
  37. sgis/raster/base.py +208 -0
  38. sgis/raster/cube.py +54 -8
  39. sgis/raster/image_collection.py +3257 -0
  40. sgis/raster/indices.py +17 -5
  41. sgis/raster/raster.py +138 -243
  42. sgis/raster/sentinel_config.py +120 -0
  43. sgis/raster/zonal.py +0 -1
  44. {ssb_sgis-1.0.2.dist-info → ssb_sgis-1.0.4.dist-info}/METADATA +6 -7
  45. ssb_sgis-1.0.4.dist-info/RECORD +62 -0
  46. sgis/raster/methods_as_functions.py +0 -0
  47. sgis/raster/torchgeo.py +0 -171
  48. ssb_sgis-1.0.2.dist-info/RECORD +0 -61
  49. {ssb_sgis-1.0.2.dist-info → ssb_sgis-1.0.4.dist-info}/LICENSE +0 -0
  50. {ssb_sgis-1.0.2.dist-info → ssb_sgis-1.0.4.dist-info}/WHEEL +0 -0
sgis/maps/thematicmap.py CHANGED
@@ -10,8 +10,10 @@ import numpy as np
10
10
  import pandas as pd
11
11
  from geopandas import GeoDataFrame
12
12
 
13
+ from .legend import LEGEND_KWARGS
13
14
  from .legend import ContinousLegend
14
15
  from .legend import Legend
16
+ from .legend import prettify_bins
15
17
  from .map import Map
16
18
 
17
19
  # the geopandas._explore raises a deprication warning. Ignoring for now.
@@ -20,60 +22,132 @@ warnings.filterwarnings(
20
22
  )
21
23
  pd.options.mode.chained_assignment = None
22
24
 
25
+ MAP_KWARGS = {
26
+ "bins",
27
+ "title",
28
+ "title_fontsize",
29
+ "size",
30
+ "cmap",
31
+ "cmap_start",
32
+ "cmap_stop",
33
+ "scheme",
34
+ "k",
35
+ "column",
36
+ "title_color",
37
+ "facecolor",
38
+ "labelcolor",
39
+ "nan_color",
40
+ "title_kwargs",
41
+ "bg_gdf_color",
42
+ "title_position",
43
+ }
44
+
23
45
 
24
46
  class ThematicMap(Map):
25
- """Class for creating static maps with geopandas and matplotlib.
26
-
27
- The class takes one or more GeoDataFrames and a column name. The class attributes
28
- can then be set to customise the map before plotting.
29
-
30
- Attributes:
31
- size (int): Width and height of the plot in inches.
32
- k (int): Number of color groups.
33
- legend (Legend): The legend object of the map. The legend holds its own set of
34
- attributes. See the Legend class for details.
35
- title (str): Title of the plot.
36
- title_color (str): Color of the title font.
37
- title_fontsize (int): Color of the title font.
38
- bins (list[int | float]): For numeric columns. List of numbers that define the
39
- maximum value for the color groups.
40
- cmap (str): Colormap of the plot. See:
47
+ """Class for making static maps.
48
+
49
+ Args:
50
+ *gdfs: One or more GeoDataFrames.
51
+ column: The name of the column to plot.
52
+ title: Title of the plot.
53
+ title_position: Title position. Either "center" (default), "left" or "right".
54
+ size: Width and height of the plot in inches. Fontsize of title and legend is
55
+ adjusted accordingly. Defaults to 25.
56
+ dark: If False (default), the background will be white and the text black. If
57
+ True, the background will be black and the text white. When True, the
58
+ default cmap is "viridis", and when False, the default is red to purple
59
+ (RdPu).
60
+ cmap: Colormap of the plot. See:
41
61
  https://matplotlib.org/stable/tutorials/colors/colormaps.html
42
- cmap_start (int): Start position for the color palette.
43
- cmap_stop (int): End position for the color palette.
44
- facecolor (str): Background color.
62
+ scheme: How to devide numeric values into categories. Defaults to
63
+ "naturalbreaks".
64
+ k: Number of color groups.
65
+ bins: For numeric columns. List of numbers that define the
66
+ maximum value for the color groups.
67
+ nan_label: Label for missing data.
68
+ legend_kwargs: dictionary with attributes for the legend. E.g.:
69
+ title: Legend title. Defaults to the column name.
70
+ rounding: If positive number, it will round floats to n decimals.
71
+ If negative, eg. -2, the number 3429 is rounded to 3400.
72
+ By default, the rounding depends on the column's maximum value
73
+ and standard deviation.
74
+ position: The legend's x and y position in the plot. By default, it's
75
+ decided dynamically by finding the space with most distance to
76
+ the geometries. To be specified as a tuple of
77
+ x and y position between 0 and 1. E.g. position=(0.8, 0.2) for a position
78
+ in the bottom right corner, (0.2, 0.8) for the upper left corner.
79
+ pretty_labels: Whether to capitalize words in text categories.
80
+ label_suffix: For numeric columns. The text to put after each number
81
+ in the legend labels. Defaults to None.
82
+ label_sep: For numeric columns. Text to put in between the two numbers
83
+ in each color group in the legend. Defaults to '-'.
84
+ thousand_sep: For numeric columns. Separator between each thousand for
85
+ large numbers. Defaults to None, meaning no separator.
86
+ decimal_mark: For numeric columns. Text to use as decimal point.
87
+ Defaults to None, meaning '.' (dot) unless 'thousand_sep' is
88
+ '.'. In this case, ',' (comma) will be used as decimal mark.
89
+ **kwargs: Additional attributes for the map. E.g.:
90
+ title_color (str): Color of the title font.
91
+ title_fontsize (int): Color of the title font.
92
+ cmap_start (int): Start position for the color palette.
93
+ cmap_stop (int): End position for the color palette.
94
+ facecolor (str): Background color.
95
+ labelcolor (str): Color for the labels.
96
+ nan_color: Color for missing data.
45
97
 
46
98
  Examples:
47
- --------
99
+ ---------
48
100
  >>> import sgis as sg
49
- >>> points = sg.random_points(100).pipe(sg.buff, np.random.rand(100))
50
- >>> points2 = sg.random_points(100).pipe(sg.buff, np.random.rand(100))
101
+ >>> points = sg.random_points(100, loc=1000).pipe(sg.buff, np.random.rand(100) * 100)
102
+ >>> points2 = sg.random_points(100, loc=1000).pipe(sg.buff, np.random.rand(100) * 100)
103
+
51
104
 
52
105
  Simple plot with legend and title.
53
106
 
54
- >>> m = sg.ThematicMap(points, points2, "area")
55
- >>> m.title = "Area of random circles"
107
+ >>> m = sg.ThematicMap(points, points2, column="area", title="Area of random circles")
56
108
  >>> m.plot()
57
109
 
58
- Plot with custom legend units (label_suffix) and separator (label_sep).
59
-
60
- >>> m = sg.ThematicMap(points, points2, "area")
61
- >>> m.title = "Area of random circles"
62
- >>> m.legend.label_suffix = "m2"
63
- >>> m.legend.label_sep = "to"
110
+ Plot with custom legend units (label_suffix) and thousand separator.
111
+ And with rounding set to -2, meaning e.g. 3429 is rounded to 3400.
112
+ If rounding was set to positive 2, 3429 would be rounded to 3429.00.
113
+
114
+ >>> m = sg.ThematicMap(
115
+ ... points,
116
+ ... points2,
117
+ ... column="area",
118
+ ... title = "Area of random circles",
119
+ ... legend_kwargs=dict(
120
+ ... rounding=-2,
121
+ ... thousand_sep=" ",
122
+ ... label_sep="to",
123
+ ... ),
124
+ ... )
64
125
  >>> m.plot()
65
126
 
66
- With custom bins and legend labels.
67
-
68
- >>> m = sg.ThematicMap(points, points2, "area")
69
- >>> m.title = "Area of random circles"
70
- >>> m.bins = [1, 2, 3]
71
- >>> m.legend.labels = [
72
- ... f"{int(round(min(points.length),0))} to 1",
73
- ... "1 to 2",
74
- ... "2 to 3",
75
- ... f"3 to {int(round(max(points.length),0))}",
76
- ... ]
127
+ With custom bins for the categories, and other customizations.
128
+
129
+ >>> m = sg.ThematicMap(
130
+ ... points,
131
+ ... points2,
132
+ ... column="area",
133
+ ... cmap="Greens",
134
+ ... cmap_start=50,
135
+ ... cmap_stop=255,
136
+ ... nan_label="Missing",
137
+ ... title = "Area of random circles",
138
+ ... bins = [5000, 10000, 15000, 20000],
139
+ ... title_kwargs=dict(
140
+ ... loc="left",
141
+ ... y=0.93,
142
+ ... x=0.025,
143
+ ... ),
144
+ ... legend_kwargs=dict(
145
+ ... thousand_sep=" ",
146
+ ... label_sep="to",
147
+ ... decimal_mark=".",
148
+ ... label_suffix="m2",
149
+ ... ),
150
+ ... )
77
151
  >>> m.plot()
78
152
  """
79
153
 
@@ -81,37 +155,105 @@ class ThematicMap(Map):
81
155
  self,
82
156
  *gdfs: GeoDataFrame,
83
157
  column: str | None = None,
158
+ title: str | None = None,
159
+ title_position: tuple[float, float] | None = None,
84
160
  size: int = 25,
85
- black: bool = False,
161
+ dark: bool = False,
162
+ cmap: str | None = None,
163
+ scheme: str = "naturalbreaks",
164
+ k: int = 5,
165
+ bins: tuple[float] | None = None,
166
+ nan_label: str = "Missing",
167
+ legend_kwargs: dict | None = None,
168
+ title_kwargs: dict | None = None,
169
+ legend: bool = False,
170
+ **kwargs,
86
171
  ) -> None:
87
- """Initialiser.
88
-
89
- Args:
90
- *gdfs: One or more GeoDataFrames.
91
- column: The name of the column to plot.
92
- size: Width and height of the plot in inches. Fontsize of title and legend is
93
- adjusted accordingly. Defaults to 25.
94
- black: If False (default), the background will be white and the text black. If
95
- True, the background will be black and the text white. When True, the
96
- default cmap is "viridis", and when False, the default is red to purple
97
- (RdPu).
172
+ """Initialiser."""
173
+ super().__init__(
174
+ *gdfs,
175
+ column=column,
176
+ scheme=scheme,
177
+ k=k,
178
+ bins=bins,
179
+ nan_label=nan_label,
180
+ )
98
181
 
99
- """
100
- super().__init__(*gdfs, column=column)
182
+ if not legend:
183
+ self.legend = None
101
184
 
185
+ self.title = title
102
186
  self._size = size
103
- self._black = black
187
+ self._dark = dark
188
+ self.title_kwargs = title_kwargs or {}
189
+ if title_position and "position" in self.title_kwargs:
190
+ raise TypeError(
191
+ "Specify either 'title_position' or title_kwargs position, not both."
192
+ )
193
+ if title_position or "position" in self.title_kwargs:
194
+ position = self.title_kwargs.pop("position", title_position)
195
+ error_mess = (
196
+ "legend_kwargs position should be a two length tuple/list with two numbers between "
197
+ "0 and 1 (x, y position)"
198
+ )
199
+ if not hasattr(position, "__len__"):
200
+ raise TypeError(error_mess)
201
+ if len(position) != 2:
202
+ raise ValueError(error_mess)
203
+ x, y = position
204
+ if "loc" not in self.title_kwargs:
205
+ if x < 0.4:
206
+ self.title_kwargs["loc"] = "left"
207
+ elif x > 0.6:
208
+ self.title_kwargs["loc"] = "right"
209
+ else:
210
+ self.title_kwargs["loc"] = "center"
211
+
212
+ self.title_kwargs["x"], self.title_kwargs["y"] = x, y
104
213
  self.background_gdfs = []
105
214
 
106
- self._title_fontsize = self._size * 2
215
+ legend_kwargs = legend_kwargs or {}
107
216
 
108
- self.black = black
217
+ self._title_fontsize = self._size * 1.9
109
218
 
110
- if not self._is_categorical:
219
+ black = kwargs.pop("black", None)
220
+ self._dark = self._dark or black
221
+
222
+ if not self.cmap and not self._is_categorical:
111
223
  self._choose_cmap()
112
224
 
225
+ self._dark_or_light()
113
226
  self._create_legend()
114
227
 
228
+ if cmap:
229
+ self._cmap = cmap
230
+
231
+ for key, value in kwargs.items():
232
+ if key not in MAP_KWARGS:
233
+ raise TypeError(
234
+ f"{self.__class__.__name__} got an unexpected keyword argument {key}"
235
+ )
236
+ try:
237
+ setattr(self, key, value)
238
+ except Exception:
239
+ setattr(self, f"_{key}", value)
240
+
241
+ for key, value in legend_kwargs.items():
242
+ if key not in LEGEND_KWARGS:
243
+ raise TypeError(
244
+ f"{self.__class__.__name__} legend_kwargs got an unexpected key {key}"
245
+ )
246
+ if self.legend is not None:
247
+ try:
248
+ setattr(self.legend, key, value)
249
+ except Exception:
250
+ setattr(self.legend, f"_{key}", value)
251
+
252
+ @property
253
+ def valid_keywords(self) -> set[str]:
254
+ """List all valid keywords for the class initialiser."""
255
+ return MAP_KWARGS
256
+
115
257
  def change_cmap(self, cmap: str, start: int = 0, stop: int = 256) -> "ThematicMap":
116
258
  """Change the color palette of the plot.
117
259
 
@@ -177,7 +319,7 @@ class ThematicMap(Map):
177
319
  else:
178
320
  kwargs = self._prepare_continous_plot(kwargs)
179
321
  if self.legend:
180
- if not self.legend._rounding_has_been_set:
322
+ if not self.legend.rounding:
181
323
  self.legend._rounding = self.legend._get_rounding(
182
324
  array=self._gdf.loc[~self._nan_idx, self._column]
183
325
  )
@@ -194,15 +336,15 @@ class ThematicMap(Map):
194
336
  self._gdf, k=self._k + bool(len(self._nan_idx))
195
337
  )
196
338
 
197
- if __test:
198
- return
199
-
200
339
  self._prepare_plot(**kwargs)
201
340
 
202
341
  if self.legend:
203
342
  self.ax = self.legend._actually_add_legend(ax=self.ax)
204
343
 
205
- self._gdf.plot(legend=include_legend, ax=self.ax, **kwargs)
344
+ self.ax = self._gdf.plot(legend=include_legend, ax=self.ax, **kwargs)
345
+
346
+ if __test:
347
+ return self
206
348
 
207
349
  def save(self, path: str) -> None:
208
350
  """Save figure as image file.
@@ -238,9 +380,13 @@ class ThematicMap(Map):
238
380
  if hasattr(self, "_background_gdfs"):
239
381
  self._actually_add_background()
240
382
 
241
- if hasattr(self, "title") and self.title:
383
+ if self.title:
242
384
  self.ax.set_title(
243
- self.title, fontsize=self.title_fontsize, color=self.title_color
385
+ self.title,
386
+ **(
387
+ dict(fontsize=self.title_fontsize, color=self.title_color)
388
+ | self.title_kwargs
389
+ ),
244
390
  )
245
391
 
246
392
  def _prepare_continous_plot(self, kwargs: dict) -> dict:
@@ -257,6 +403,13 @@ class ThematicMap(Map):
257
403
  return kwargs
258
404
 
259
405
  else:
406
+ if self.legend and self.legend.rounding and self.legend.rounding < 0:
407
+ self.bins = prettify_bins(self.bins, self.legend.rounding)
408
+ self.bins = list({round(bin_, 5) for bin_ in self.bins})
409
+ self.bins.sort()
410
+ # self.legend._rounding_was = self.legend.rounding
411
+ # self.legend.rounding = None
412
+
260
413
  classified = self._classify_from_bins(self._gdf, bins=self.bins)
261
414
  classified_sequential = self._push_classification(classified)
262
415
  n_colors = len(np.unique(classified_sequential)) - any(self._nan_idx)
@@ -264,10 +417,13 @@ class ThematicMap(Map):
264
417
  self._bins_unique_values = self._make_bin_value_dict(
265
418
  self._gdf, classified_sequential
266
419
  )
420
+
267
421
  colorarray = self._unique_colors[classified_sequential]
268
422
  kwargs["color"] = colorarray
269
423
 
270
- if self.legend and not self.legend._rounding_has_been_set:
424
+ if (
425
+ self.legend and self.legend.rounding
426
+ ): # not self.legend._rounding_has_been_set:
271
427
  self.bins = self.legend._set_rounding(
272
428
  bins=self.bins, rounding=self.legend._rounding
273
429
  )
@@ -279,10 +435,13 @@ class ThematicMap(Map):
279
435
 
280
436
  def _prepare_categorical_plot(self, kwargs: dict) -> dict:
281
437
  """Map values to colors."""
282
- self._get_categorical_colors()
283
- colorarray = self._gdf["color"]
438
+ self._make_categories_colors_dict()
439
+ if self._gdf is not None and len(self._gdf):
440
+ self._fix_nans()
284
441
 
285
- kwargs["color"] = colorarray
442
+ if self._gdf is not None:
443
+ colorarray = self._gdf["color"]
444
+ kwargs["color"] = colorarray
286
445
  return kwargs
287
446
 
288
447
  def _actually_add_legend(self) -> None:
@@ -309,8 +468,10 @@ class ThematicMap(Map):
309
468
 
310
469
  def _create_legend(self) -> None:
311
470
  """Instantiate the Legend class."""
471
+ if self.legend is None:
472
+ return
312
473
  kwargs = {}
313
- if self._black:
474
+ if self._dark:
314
475
  kwargs["facecolor"] = "#0f0f0f"
315
476
  kwargs["labelcolor"] = "#fefefe"
316
477
  kwargs["title_color"] = "#fefefe"
@@ -322,7 +483,7 @@ class ThematicMap(Map):
322
483
 
323
484
  def _choose_cmap(self) -> None:
324
485
  """Kwargs is to catch start and stop points for the cmap in __init__."""
325
- if self._black:
486
+ if self._dark:
326
487
  self._cmap = "viridis"
327
488
  self.cmap_start = 0
328
489
  self.cmap_stop = 256
@@ -352,8 +513,8 @@ class ThematicMap(Map):
352
513
  ax = fig.add_subplot(1, 1, 1)
353
514
  return fig, ax
354
515
 
355
- def _black_or_white(self) -> None:
356
- if self._black:
516
+ def _dark_or_light(self) -> None:
517
+ if self._dark:
357
518
  self.facecolor, self.title_color, self.bg_gdf_color = (
358
519
  "#0f0f0f",
359
520
  "#fefefe",
@@ -367,23 +528,22 @@ class ThematicMap(Map):
367
528
  self.facecolor, self.title_color, self.bg_gdf_color = (
368
529
  "#fefefe",
369
530
  "#0f0f0f",
370
- "#ebebeb",
531
+ "#dbdbdb",
371
532
  )
372
533
  self.nan_color = "#c2c2c2"
373
534
  if not self._is_categorical:
374
535
  self.change_cmap("RdPu", start=23)
375
536
 
376
- self._create_legend()
377
-
378
537
  @property
379
- def black(self) -> bool:
538
+ def dark(self) -> bool:
380
539
  """Whether to use dark background and light text colors."""
381
- return self._black
540
+ return self._dark
382
541
 
383
- @black.setter
384
- def black(self, new_value: bool):
385
- self._black = new_value
386
- self._black_or_white()
542
+ @dark.setter
543
+ def dark(self, new_value: bool):
544
+ self._dark = new_value
545
+ self._dark_or_light()
546
+ self._create_legend()
387
547
 
388
548
  @property
389
549
  def title_fontsize(self) -> int:
@@ -5,6 +5,7 @@ from igraph import Graph
5
5
  from shapely import force_2d
6
6
  from shapely import reverse
7
7
  from shapely import unary_union
8
+ from shapely import union_all
8
9
  from shapely.geometry import MultiPoint
9
10
  from shapely.geometry import Point
10
11
  from shapely.ops import nearest_points
@@ -113,7 +114,11 @@ def _service_area(
113
114
  else:
114
115
  snapped_origin: Point = nearest_points(
115
116
  nodes_union,
116
- origins.loc[origins["temp_idx"] == idx, "geometry"].unary_union,
117
+ union_all(
118
+ origins.loc[
119
+ origins["temp_idx"] == idx, "geometry"
120
+ ].geometry.values
121
+ ),
117
122
  )[0]
118
123
 
119
124
  within = sfilter(within, snapped_origin.buffer(0.01))
@@ -44,7 +44,7 @@ def close_network_holes(
44
44
  NetworkAnalysis. These values must be filled before analysis.
45
45
 
46
46
  Examples:
47
- --------
47
+ ---------
48
48
  Read road data with small gaps.
49
49
 
50
50
  >>> import sgis as sg
@@ -157,7 +157,7 @@ def close_network_holes_to_deadends(
157
157
  The input GeoDataFrame with new lines added.
158
158
 
159
159
  Examples:
160
- --------
160
+ ---------
161
161
  Read road data with small gaps.
162
162
 
163
163
  >>> import sgis as sg
@@ -52,7 +52,7 @@ def split_lines_by_nearest_point(
52
52
  ValueError: If the crs of the input data differs.
53
53
 
54
54
  Examples:
55
- --------
55
+ ---------
56
56
  >>> from sgis import read_parquet_url, split_lines_by_nearest_point
57
57
  >>> roads = read_parquet_url("https://media.githubusercontent.com/media/statisticsnorway/ssb-sgis/main/tests/testdata/roads_oslo_2022.parquet")
58
58
  >>> points = read_parquet_url("https://media.githubusercontent.com/media/statisticsnorway/ssb-sgis/main/tests/testdata/points_oslo.parquet")
@@ -77,7 +77,7 @@ def split_lines_by_nearest_point(
77
77
  """
78
78
  PRECISION = 1e-6
79
79
 
80
- if not len(gdf):
80
+ if not len(gdf) or not len(points):
81
81
  return gdf
82
82
 
83
83
  if (points.crs is not None and gdf.crs is not None) and not points.crs.equals(
@@ -86,10 +86,14 @@ def split_lines_by_nearest_point(
86
86
  raise ValueError("crs mismatch:", points.crs, "and", gdf.crs)
87
87
 
88
88
  if get_geom_type(gdf) != "line":
89
- raise ValueError("'gdf' should only have line geometries.", gdf.geom_type)
89
+ raise ValueError(
90
+ f"'gdf' should only have line geometriess. Got {gdf.geom_type.value_counts()}"
91
+ )
90
92
 
91
93
  if get_geom_type(points) != "point":
92
- raise ValueError("'points' should only have point geometries.")
94
+ raise ValueError(
95
+ f"'points' should only have point geometries. Got {points.geom_type.value_counts()}"
96
+ )
93
97
 
94
98
  gdf = gdf.copy()
95
99
 
@@ -230,9 +234,12 @@ def _change_line_endpoint(
230
234
  .values
231
235
  )
232
236
 
233
- relevant_lines_mapped = relevant_lines.groupby(level=0)["geometry"].agg(LineString)
237
+ is_line = relevant_lines.groupby(level=0).size() > 1
238
+ relevant_lines_mapped = (
239
+ relevant_lines.loc[is_line].groupby(level=0)["geometry"].agg(LineString)
240
+ )
234
241
 
235
- gdf.loc[is_relevant, "geometry"] = relevant_lines_mapped
242
+ gdf.loc[relevant_lines_mapped.index, "geometry"] = relevant_lines_mapped
236
243
 
237
244
  return gdf
238
245
 
@@ -255,7 +262,7 @@ def cut_lines(
255
262
  This method is time consuming for large networks and low 'max_length'.
256
263
 
257
264
  Examples:
258
- --------
265
+ ---------
259
266
  >>> from sgis import read_parquet_url, cut_lines
260
267
  >>> roads = read_parquet_url("https://media.githubusercontent.com/media/statisticsnorway/ssb-sgis/main/tests/testdata/roads_oslo_2022.parquet")
261
268
  >>> roads.length.describe().round(1)
@@ -327,7 +334,7 @@ def cut_lines_once(
327
334
  Defaults to False.
328
335
 
329
336
  Examples:
330
- --------
337
+ ---------
331
338
  >>> from sgis import cut_lines_once, to_gdf
332
339
  >>> import pandas as pd
333
340
  >>> from shapely.geometry import LineString
@@ -29,7 +29,7 @@ def make_directed_network_norway(gdf: GeoDataFrame, dropnegative: bool) -> GeoDa
29
29
  to keep them.
30
30
 
31
31
  Examples:
32
- --------
32
+ ---------
33
33
  2022 data for the municipalities of Oslo and Eidskog can be read directly like this:
34
34
 
35
35
  >>> import sgis as sg
@@ -1,6 +1,7 @@
1
1
  """Functions for Finding network components in a GeoDataFrame of lines."""
2
2
 
3
3
  import networkx as nx
4
+ import pandas as pd
4
5
  from geopandas import GeoDataFrame
5
6
 
6
7
  from .nodes import make_node_ids
@@ -23,7 +24,7 @@ def get_connected_components(gdf: GeoDataFrame) -> GeoDataFrame:
23
24
  The GeoDataFrame with a new column "connected".
24
25
 
25
26
  Examples:
26
- --------
27
+ ---------
27
28
  >>> from sgis import read_parquet_url, get_connected_components
28
29
  >>> roads = read_parquet_url("https://media.githubusercontent.com/media/statisticsnorway/ssb-sgis/main/tests/testdata/roads_oslo_2022.parquet")
29
30
 
@@ -76,12 +77,12 @@ def get_component_size(gdf: GeoDataFrame) -> GeoDataFrame:
76
77
  A GeoDataFrame with a new column "component_size".
77
78
 
78
79
  Examples:
79
- --------
80
+ ---------
80
81
  >>> from sgis import read_parquet_url, get_component_size
81
82
  >>> roads = read_parquet_url("https://media.githubusercontent.com/media/statisticsnorway/ssb-sgis/main/tests/testdata/roads_oslo_2022.parquet")
82
83
 
83
84
  >>> roads = get_component_size(roads)
84
- >>> roads.component_size.value_counts().head()
85
+ >>> roads["component_size"].value_counts().head()
85
86
  component_size
86
87
  79180 85638
87
88
  2 1601
@@ -101,11 +102,17 @@ def get_component_size(gdf: GeoDataFrame) -> GeoDataFrame:
101
102
  graph.add_edges_from(edges)
102
103
  components = [list(x) for x in nx.connected_components(graph)]
103
104
 
104
- componentsdict = {
105
- idx: len(component) for component in components for idx in component
106
- }
107
-
108
- gdf["component_size"] = gdf.source.map(componentsdict)
105
+ mapper = pd.DataFrame(
106
+ {
107
+ idx: [i, len(component)]
108
+ for i, component in enumerate(components)
109
+ for idx in component
110
+ },
111
+ ).transpose()
112
+ mapper.columns = ["component_index", "component_size"]
113
+
114
+ gdf["component_index"] = gdf["source"].map(mapper["component_index"])
115
+ gdf["component_size"] = gdf["source"].map(mapper["component_size"])
109
116
 
110
117
  gdf = gdf.drop(
111
118
  ["source_wkt", "target_wkt", "source", "target", "n_source", "n_target"], axis=1