¿Disolver polígonos basados ​​en atributos con Python (bien proporcionado, fiona)?

15

He estado tratando de crear una función que básicamente haga lo mismo que la función de "disolución" de QGIS. Pensé que sería súper fácil pero al parecer no. Entonces, por lo que he reunido, el uso de fiona con bien proporcionado debería ser la mejor opción aquí. Empecé a jugar con archivos vectoriales, por lo que este mundo es bastante nuevo para mí y para Python también.

Para este ejemplo, estoy trabajando con un archivo de forma del condado fundado aquí http://tinyurl.com/odfbanu Así que aquí hay un código que reuní pero no puedo encontrar una manera de hacer que trabajen juntos

Por ahora, mi mejor método es el siguiente basado en: https://sgillies.net/2009/01/27/a-more-perfect-union-continued.html . Funciona bien y obtengo una lista de los 52 estados como geometría Shapely. Siéntase libre de comentar si hay una manera más directa de hacer esta parte.

from osgeo import ogr
from shapely.wkb import loads
from numpy import asarray
from shapely.ops import cascaded_union

ds = ogr.Open('counties.shp')
layer = ds.GetLayer(0)

#create a list of unique states identifier to be able
#to loop through them later
STATEFP_list = []
for i in range(0 , layer.GetFeatureCount()) :
    feature = layer.GetFeature(i)
    statefp = feature.GetField('STATEFP')
    STATEFP_list.append(statefp)

STATEFP_list = set(STATEFP_list)

#Create a list of merged polygons = states 
#to be written to file
polygons = []

#do the actual dissolving based on STATEFP
#and append polygons
for i in STATEFP_list : 
    county_to_merge = []
    layer.SetAttributeFilter("STATEFP = '%s'" %i ) 
    #I am not too sure why "while 1" but it works 
    while 1:
        f = layer.GetNextFeature()
        if f is None: break
        g = f.geometry()
        county_to_merge.append(loads(g.ExportToWkb()))

    u = cascaded_union(county_to_merge)
    polygons.append(u)

#And now I am totally stuck, I have no idea how to write 
#this list of shapely geometry into a shapefile using the
#same properties that my source.

Entonces, la escritura no es sencilla de lo que he visto, realmente solo quiero que el mismo archivo de forma solo con el país se disuelva en estados, ni siquiera necesito mucho de la tabla de atributos, pero tengo curiosidad por ver cómo puedes pasar desde la fuente hasta el nuevo shapefile creado.

Encontré muchas piezas de código para escribir con Fiona, pero nunca puedo hacer que funcione con mis datos. Ejemplo de ¿Cómo escribir geometrías Shapely en shapefiles? :

from shapely.geometry import mapping, Polygon
import fiona

# Here's an example Shapely geometry
poly = Polygon([(0, 0), (0, 1), (1, 1), (0, 0)])

# Define a polygon feature geometry with one attribute
schema = {
    'geometry': 'Polygon',
    'properties': {'id': 'int'},
}

# Write a new Shapefile
with fiona.open('my_shp2.shp', 'w', 'ESRI Shapefile', schema) as c:
    ## If there are multiple geometries, put the "for" loop here
    c.write({
        'geometry': mapping(poly),
        'properties': {'id': 123},
    })

El problema aquí es cómo hacer lo mismo con una lista de geometría y cómo recrear las mismas propiedades que la fuente.

Usuario18981898198119
fuente

Respuestas:

22

La pregunta es sobre Fiona y Shapely y la otra respuesta usando GeoPandas requiere también conocer Pandas . Además, GeoPandas usa Fiona para leer / escribir archivos de forma.

No cuestiono aquí la utilidad de GeoPandas, pero puede hacerlo directamente con Fiona utilizando las herramientas de módulo estándar , especialmente con el comando groupby ("En pocas palabras, groupby toma un iterador y lo divide en sub-iteradores en función de los cambios en la "clave" del iterador principal. Esto, por supuesto, se hace sin leer todo el iterador fuente en la memoria ", itertools.groupby ).

Shapefile original coloreado por el campo STATEFP

ingrese la descripción de la imagen aquí

from shapely.geometry import shape, mapping
from shapely.ops import unary_union
import fiona
import itertools
with fiona.open('cb_2013_us_county_20m.shp') as input:
    # preserve the schema of the original shapefile, including the crs
    meta = input.meta
    with fiona.open('dissolve.shp', 'w', **meta) as output:
        # groupby clusters consecutive elements of an iterable which have the same key so you must first sort the features by the 'STATEFP' field
        e = sorted(input, key=lambda k: k['properties']['STATEFP'])
        # group by the 'STATEFP' field 
        for key, group in itertools.groupby(e, key=lambda x:x['properties']['STATEFP']):
            properties, geom = zip(*[(feature['properties'],shape(feature['geometry'])) for feature in group])
            # write the feature, computing the unary_union of the elements in the group with the properties of the first element in the group
            output.write({'geometry': mapping(unary_union(geom)), 'properties': properties[0]})

Resultado

ingrese la descripción de la imagen aquí

gene
fuente
Bonito también, difícil de elegir entre los dos. Es bueno ver diferentes métodos, gracias!
Usuario18981898198119
11

Recomiendo GeoPandas para tratar grandes surtidos de características y realizar operaciones masivas.

Extiende los marcos de datos de Pandas y los utiliza de forma bien debajo del capó.

from geopandas import GeoSeries, GeoDataFrame

# define your directories and file names
dir_input = '/path/to/your/file/'
name_in = 'cb_2013_us_county_20m.shp'
dir_output = '/path/to/your/file/'
name_out = 'states.shp'

# create a dictionary
states = {}
# open your file with geopandas
counties = GeoDataFrame.from_file(dir_input + name_in)

for i in range(len(counties)):
    state_id = counties.at[i, 'STATEFP']
    county_geometry = counties.at[i, 'geometry']
    # if the feature's state doesn't yet exist, create it and assign a list
    if state_id not in states:
        states[state_id] = []
    # append the feature to the list of features
    states[state_id].append(county_geometry)

# create a geopandas geodataframe, with columns for state and geometry
states_dissolved = GeoDataFrame(columns=['state', 'geometry'], crs=counties.crs)

# iterate your dictionary
for state, county_list in states.items():
    # create a geoseries from the list of features
    geometry = GeoSeries(county_list)
    # use unary_union to join them, thus returning polygon or multi-polygon
    geometry = geometry.unary_union
    # set your state and geometry values
    states_dissolved.set_value(state, 'state', state)
    states_dissolved.set_value(state, 'geometry', geometry)

# save to file
states_dissolved.to_file(dir_output + name_out, driver="ESRI Shapefile")
Songololo
fuente
Eso es mucho más elegante que mi cosa rara y rara. Gracias ! ¿Hay alguna manera de transmitir el sistema de referencia espacial?
Usuario18981898198119
He editado mi publicación para mostrar cómo transferir los crs del archivo fuente al nuevo archivo. Esto sucede en la línea donde se crea el GeoDataFrame states_dissolved. Con respecto al esquema, sugeriría simplemente crear uno manualmente (es decir, usar las propiedades de las columnas de la misma línea) que luego se escriben en las propiedades del nuevo archivo. es decir, cuando cree su diccionario de estados, simplemente agregue otras propiedades a medida que avanza y asígnelas a una columna en el nuevo marco de datos.
songololo
0

Como un apéndice a la respuesta de @ gene , necesitaba disolverlo en más de un campo, por lo que modifiqué su código para manejar múltiples campos. El siguiente código se utiliza operator.itemgetterpara agrupar por múltiples campos:

# Modified from /gis//a/150001/2856
from shapely.geometry import shape, mapping
from shapely.ops import unary_union
import fiona
import itertools
from operator import itemgetter


def dissolve(input, output, fields):
    with fiona.open(input) as input:
        with fiona.open(output, 'w', **input.meta) as output:
            grouper = itemgetter(*fields)
            key = lambda k: grouper(k['properties'])
            for k, group in itertools.groupby(sorted(input, key=key), key):
                properties, geom = zip(*[(feature['properties'], shape(feature['geometry'])) for feature in group])
                output.write({'geometry': mapping(unary_union(geom)), 'properties': properties[0]})


if __name__ == '__main__':
    dissolve('input.shp', 'input_dissolved.shp', ["FIELD1", "FIELD2", "FIELDN"))
usuario2856
fuente