Search code examples
pythongeopandasshapely

Spatially dissolve lines that intersect, after attending tabular conditions


I need to dissolve all Linestrings from a geodataframe that intersect each other after attending some tabular conditions: 1)have the same ID and 2) have the same opening year. The data represent a road database and I am working with geopandas. The data look like this:

    id    opening    geometry
0   30    2020       LINESTRING (45.01679 -12.12937, 45.01681 -12...
1   101   1999       LINESTRING (37.02849 -10.65968, 37.02849 -10...
2   30    2019       LINESTRING (47.10667 -15.49339, 47.10665 -15...
3   101   1999       LINESTRING (41.64170 -12.45764, 41.64180 -12...
4   135   2020       LINESTRING (45.31902 -9.76800, 45.31907 -9.7...

I have tried the following code:

import geopandas as gpd
import numpy as np
import shapely

gdf_sof = gpd.read_file('your_path/your_file')

# Concatenate the conected segments
gdf = gpd.GeoDataFrame()
for id in gdf_sof.id.unique():
  print(id)
  unique_id = gdf_sof.loc[gdf_sof.id == id]
  print(unique_id.shape)
  gdf_sof_id = gpd.GeoDataFrame()
  for dt in unique_id.opening.unique():
    print(dt)
    unique_oppening = unique_id.loc[unique_id.opening == dt]
    dissolved = gpd.geoseries.GeoSeries([geom for geom in unique_oppening.unary_union.geoms])
    gdf_sof_id = pd.concat([gdf_sof_id, dissolved], ignore_index=True, axis=0)
  gdf.concat(gdf_sof_id, ignore_index=True, axis=0, inplace=True)

gdf.shape

But got this error message:

---------------------------------------------------------------------------
AttributeError: 'LineString' object has no attribute 'geoms'

I also tried the following code based on @Pieter's comment, but the result isn't satisfactory. I used both linemerge and shapely.get_parts.

import geopandas as gpd
import numpy as np
import pandas as pd
import shapely
from shapely.ops import linemerge

gdf_sof = gpd.read_file('your_path/your_file')

# Dissolve the connected segments
gdf = gpd.GeoDataFrame()
for id in gdf_sof.id.unique():
  unique_id = gdf_sof.loc[gdf_sof.id == id]
  gdf_sof_id = gpd.GeoDataFrame()
  for dt in unique_id.opening.unique():
    unique_opening = unique_id.loc[unique_id.opening == dt]
    shp = shapely.get_parts(unique_opening.geometry)
    dissolved = linemerge(shp)
    gdf_dissolved = gpd.GeoDataFrame({'id': [id],
                                      'opening': [dt],
                                      'geometry':dissolved},
                                      geometry = 'geometry', 
                                      crs = gdf_sof.crs)
    gdf_sof_id = pd.concat([gdf_sof_id, gdf_dissolved], ignore_index=True, axis=0)
  gdf = pd.concat([gdf, gdf_sof_id], ignore_index=True, axis=0)

gdf.shape

My starting gdf (gdf_sof) has the shape (7366, 3), and the final gdf has a shape of (973, 3). However, the final gdf (973,3) dissolved segments that don't touch.

All highlighted segments in the image are just one row in the attribute table. I need them separated!

All highlighted segments in the image above are just one row in the attribute table. I need them separated!


Solution

  • I found a solution:

    # Marge segments based on tabular info ('id' and 'dt')
    gdf = gpd.GeoDataFrame()
    for id in gdf_sof.id.unique():
      unique_id = gdf_sof.loc[gdf_sof.id == id]
      gdf_sof_id = gpd.GeoDataFrame()
      for dt in unique_id.opening.unique():
        unique_opening = unique_id.loc[unique_id.opening == dt]
        shp = shapely.get_parts(unique_opening.geometry)
        dissolved = linemerge(shp)
        gdf_dissolved = gpd.GeoDataFrame({'id': [id],
                                          'opening': [dt],
                                          'geometry':dissolved},
                                          geometry = 'geometry',
                                          crs = gdf_sof.crs)
        gdf_sof_id = pd.concat([gdf_sof_id, gdf_dissolved], ignore_index=True, axis=0)
      gdf = pd.concat([gdf, gdf_sof_id], ignore_index=True, axis=0)
    
    # show the shape
    gdf.shape
    

    The gdf has few lines. The linemerge() function merged segments that don't touch each other, as shown in the second part of the question.

    Then, we need to separate parts in geoms:

    gdf_final = gpd.GeoDataFrame()
    for mls in gdf.geometry.index:
      #print(mls)
      #print(gdf.geometry[mls].geom_type)
      if gdf.geometry[mls].geom_type == 'MultiLineString':
        gdf_multi = gpd.GeoDataFrame()
        for part in gdf.geometry[mls].geoms:
          gdf_part = gpd.GeoDataFrame({'id': [gdf.id[mls]],
                                       'opening': [gdf.opening[mls]],
                                       'geometry':part},
                                      geometry = 'geometry',
                                      crs = gdf_sof.crs)
          gdf_multi = pd.concat([gdf_multi, gdf_part], ignore_index=True, axis=0)
        gdf_final = pd.concat([gdf_final, gdf_multi], ignore_index=True, axis=0)
      else:
        gdf_final = pd.concat([gdf_final, gdf.iloc[mls:mls+1]], ignore_index=True, axis=0)
    
    gdf_final