Ejercicio 8: Asignación de población a clínicas
Este ejercicio combina capas vectoriales y ráster para estimar, a partir de la ubicación de clínicas y un límite municipal, tres cifras de población: la población total del municipio, la población atendida por el conjunto de clínicas (dentro de un radio de servicio) y la población asignada a cada clínica en particular (mediante polígonos de Voronoi).
Conceptos clave de POO y PyQGIS usados en el ejercicio
-
Tres entradas de distinto tipo: a diferencia de los
ejercicios anteriores, aquí
initAlgorithmcombina dos capas vectoriales (QgsProcessingParameterVectorLayer) y una capa ráster (QgsProcessingParameterRasterLayer) como entradas de un mismo algoritmo. -
Tres salidas vectoriales:
QgsProcessingParameterVectorDestinationse usa 3 veces para declarar los 3 archivos de salida (población total, atendida y asignada), mostrando que una herramienta puede entregar varios resultados vectoriales distintos, no solo uno. -
Encadenar herramientas de análisis vectorial: el
ejercicio combina 4 herramientas nativas distintas de QGIS
(
native:buffer,native:voronoipolygons,native:clipynative:zonalstatisticsfb), usando el resultado de unas como entrada de las siguientes, igual que en los "modelos" de los ejercicios 1 a 3, pero aplicado a análisis vectorial en vez de reclasificación ráster. -
Estadísticas de zona (
zonalstatisticsfb): esta herramienta resume los valores de una capa ráster (aquí, la distribución de población) dentro de cada polígono de una capa vectorial, agregando el resultado como una nueva columna en la tabla de atributos. Se usa 3 veces con capas de entrada distintas (municipio completo, buffer recortado, Voronoi recortado) para obtener las 3 cifras de población. -
Feedback de 7 pasos:
QgsProcessingMultiStepFeedback(7, model_feedback)porque el modelo tiene 7 sub-procesos: generar el buffer, generar el Voronoi, recortar ambos al municipio, y calcular las 3 estadísticas de zona.
Análisis y explicación de la estructura y lógica del código
"""
Herramienta de script para la asignación de población a clínicas
Pasos generales:
1. Generar un buffer de 5,000 m a partir de los puntos de clínicas.
2. Generar polígonos de Voronoi a partir de los puntos de clínicas.
3. Recortar el buffer al límite del municipio (mask).
4. Recortar los polígonos de Voronoi al límite del municipio (mask).
5. Estadísticas de zona del límite municipal sobre la capa de distribución
de la población --> Población Total del municipio.
6. Estadísticas de zona del buffer recortado sobre la capa distribución
de la población --> Población atendida por todas las clínicas.
7. Estadísticas de zona de los pológonos de Voronoi sobre la capa de distribución
de la población --> Pobación asignada a cada clínica.
Capas de entrada:
1. Vectorial de clínas (Clinicas_7089_wgs.shp).
2. Vectorial del límite municipal (Mask_7089_wgs.shp).
3. Ráster de distribución de la población (DistPob_7089_wgs.tif).
Capas de salida:
1. Vectorial con la población total del municipio.
2. Vectorial con la población atendida en el municipio.
3. Vectorial con la población asignada del municipio.
"""
#Importar las bibliotecas necesarias
from qgis.core import QgsProcessingAlgorithm
from qgis.core import QgsProcessingParameterRasterLayer
from qgis.core import QgsProcessingParameterVectorLayer
from qgis.core import QgsProcessingParameterVectorDestination
from qgis.core import QgsProcessingMultiStepFeedback
from qgis import processing
#Crear la clase para la herramienta
class Clinicas(QgsProcessingAlgorithm):
#1. Nombre de la herramienta
def name(self):
return 'clinicas'
#2. Nombre a mostrar en la herramienta
def displayName(self):
return 'Asignación de población a clínicas'
#3. Grupo al que pertence la herramienta
def group(self):
return 'Herramientas vectoriales'
#4. ID del grupo de herramientas
def groupId(self):
return 'herramientas_vectoriales'
#5. Descripción breve de la herramienta
def shortHelpString(self):
descripcion = """
A partir de los archivos de entrada:
1. Ubicación de clínicas (vectorial de puntos).
2. Límite municipal (vectorial de polígono).
3. Distribución de la población por celda (ráster).
Esta herramienta genera tres archivos de salida:
1. Un archivo vectorial con la población total del municipio.
2. Un archivo vectorial con la población del municipio atendida por cada clínica.
3. Un archivo vectorial con la población del municipio asignada a cada clínica.
"""
return descripcion
#6. Capas de entrada y salida
def initAlgorithm(self, config=None):
#======= Capas de entrada =======
#Archivo de clínicas (vectorial)
self.addParameter(QgsProcessingParameterVectorLayer('clinicas', 'Archivo vectorial de entrada con las clínicas', defaultValue=None, optional=False))
#Archivo del límite municipal (vectorial)
self.addParameter(QgsProcessingParameterVectorLayer('municipio', 'Archivo vectorial de entrada con el límite municipal', defaultValue=None, optional=False))
#Archivo de dsitribución de la población (ráster)
self.addParameter(QgsProcessingParameterRasterLayer('poblacion', 'Archivo ráster de entrada con la distribución de la población', defaultValue=None, optional=False))
#====== Capas de salida =========
#Población total del municipio
self.addParameter(QgsProcessingParameterVectorDestination('pob_total', 'Archivo vectorial de salida con la población total del municipio'))
#Población atendida por clínicas
self.addParameter(QgsProcessingParameterVectorDestination('pob_atendida', 'Archivo vectorial de salida con la población atendida por las clínicas'))
#Población asignada a las clínicas
self.addParameter(QgsProcessingParameterVectorDestination('pob_asignada', 'Archivo vectorial de salida con la población asignada a las clínicas'))
#7. Algoritmo de procesamiento
def processAlgorithm(self, parameters, context, model_feedback):
outputs = {}
results = {}
feedback = QgsProcessingMultiStepFeedback(7, model_feedback)
#Paso 1. Generar buffer a partir de las clínicas
alg_params = {
'INPUT':parameters['clinicas'],
'DISTANCE':5000,
'DISSOLVE':True,
'OUTPUT':'TEMPORARY_OUTPUT'
}
outputs['buffer'] = processing.run('native:buffer', alg_params, context=context, feedback=feedback, is_child_algorithm=True)
feedback.setCurrentStep(1)
if feedback.isCanceled():
return {}
#Paso 2. Polígonos de Voronoi a partir de las clínicas
alg_params = {
'INPUT':parameters['clinicas'],
'BUFFER':50,
'OUTPUT':'TEMPORARY_OUTPUT'
}
outputs['voronoi'] = processing.run('native:voronoipolygons', alg_params, context=context, feedback=feedback, is_child_algorithm=True)
feedback.setCurrentStep(2)
if feedback.isCanceled():
return {}
#Paso 3. Recortar el buffer al límite municipal
alg_params = {
'INPUT':outputs['buffer']['OUTPUT'],
'OVERLAY':parameters['municipio'],
'OUTPUT':'TEMPORARY_OUTPUT'
}
outputs['buffer_cortado'] = processing.run('native:clip', alg_params, context=context, feedback=feedback, is_child_algorithm=True)
feedback.setCurrentStep(3)
if feedback.isCanceled():
return {}
#Paso 4. Recortar lo polígonos de Voronoi al límite municipal
alg_params = {
'INPUT':outputs['voronoi']['OUTPUT'],
'OVERLAY':parameters['municipio'],
'OUTPUT':'TEMPORARY_OUTPUT'
}
outputs['voronoi_cortado'] = processing.run('native:clip', alg_params, context=context, feedback=feedback, is_child_algorithm=True)
feedback.setCurrentStep(4)
if feedback.isCanceled():
return {}
#Paso 5. Estadísticas de zona para la población total
alg_params = {
'INPUT':parameters['municipio'],
'INPUT_RASTER':parameters['poblacion'],
'COLUMN_PREFIX':'Pob_',
'STATISTICS':[1],
'OUTPUT':parameters['pob_total']
}
outputs['pob_total'] = processing.run('native:zonalstatisticsfb', alg_params, context=context, feedback=feedback, is_child_algorithm=True)
feedback.setCurrentStep(5)
if feedback.isCanceled():
return {}
#Paso 6. Estadísticas de zona para la poblacion atendida
alg_params = {
'INPUT':outputs['buffer_cortado']['OUTPUT'],
'INPUT_RASTER':parameters['poblacion'],
'COLUMN_PREFIX':'Pob_',
'STATISTICS':[1],
'OUTPUT':parameters['pob_atendida']
}
outputs['pob_atendida'] = processing.run('native:zonalstatisticsfb', alg_params, context=context, feedback=feedback, is_child_algorithm=True)
feedback.setCurrentStep(6)
if feedback.isCanceled():
return {}
#Paso 7. Estadísticas de zona para la poblacion asignada
alg_params = {
'INPUT':outputs['voronoi_cortado']['OUTPUT'],
'INPUT_RASTER':parameters['poblacion'],
'COLUMN_PREFIX':'Pob_',
'STATISTICS':[1],
'OUTPUT':parameters['pob_asignada']
}
outputs['pob_asignada'] = processing.run('native:zonalstatisticsfb', alg_params, context=context, feedback=feedback, is_child_algorithm=True)
#Asignación de los resultados finales al diccionario results
results['pob_total'] = outputs['pob_total']['OUTPUT']
results['pob_atendida'] = outputs['pob_atendida']['OUTPUT']
results['pob_asiganda'] = outputs['pob_asignada']['OUTPUT']
print('Diccionario de outputs:')
print(outputs)
print('Diccionario de results:')
print(results)
return results
#8. Instanciar la clase
def createInstance(self):
return self.__class__()
Sección 1 · Para qué sirve cada biblioteca importada
from qgis.core import QgsProcessingAlgorithm
from qgis.core import QgsProcessingParameterRasterLayer
from qgis.core import QgsProcessingParameterVectorLayer
from qgis.core import QgsProcessingParameterVectorDestination
from qgis.core import QgsProcessingMultiStepFeedback
from qgis import processing
QgsProcessingAlgorithm: clase base de la que debe heredar la claseClinicaspara que QGIS la reconozca como una herramienta de procesamiento ejecutable.QgsProcessingParameterRasterLayer: permite declarar la capa ráster de entrada con la distribución de la población.QgsProcessingParameterVectorLayer: permite declarar las dos capas vectoriales de entrada (clínicas y límite municipal).QgsProcessingParameterVectorDestination: a diferencia deQgsProcessingParameterRasterDestination(usado en los ejercicios 1 a 6), esta clase declara un archivo de salida vectorial en vez de uno ráster. Se usa 3 veces, una por cada resultado.QgsProcessingMultiStepFeedback: reporta el progreso de los 7 pasos internos del modelo y permite cancelar la ejecución entre cualquiera de ellos.from qgis import processing: da acceso aprocessing.run(...), usado aquí para encadenar 4 herramientas nativas distintas de análisis vectorial y ráster.
Sección 2 · Para qué sirve cada método de la clase
name(): devuelve'clinicas', el identificador interno de la herramienta.displayName(): devuelve'Asignación de población a clínicas', el nombre visible en QGIS.group()/groupId(): ubican la herramienta en el grupo'Herramientas vectoriales', el mismo grupo que el ejercicio 7.shortHelpString(): explica las 3 capas de entrada necesarias y los 3 archivos de salida que generará la herramienta.initAlgorithm(): declara las 3 entradas (2 vectoriales, 1 ráster) y las 3 salidas vectoriales.processAlgorithm(): contiene toda la lógica de análisis espacial: genera el buffer y los polígonos de Voronoi, los recorta al municipio, y calcula las 3 estadísticas de zona de población.createInstance(): permite a QGIS crear nuevas copias de la herramienta cada vez que se ejecuta.
Sección 3 · Declaración de entradas y salidas (initAlgorithm)
def initAlgorithm(self, config=None):
#======= Capas de entrada =======
#Archivo de clínicas (vectorial)
self.addParameter(QgsProcessingParameterVectorLayer('clinicas', 'Archivo vectorial de entrada con las clínicas', defaultValue=None, optional=False))
#Archivo del límite municipal (vectorial)
self.addParameter(QgsProcessingParameterVectorLayer('municipio', 'Archivo vectorial de entrada con el límite municipal', defaultValue=None, optional=False))
#Archivo de dsitribución de la población (ráster)
self.addParameter(QgsProcessingParameterRasterLayer('poblacion', 'Archivo ráster de entrada con la distribución de la población', defaultValue=None, optional=False))
#====== Capas de salida =========
#Población total del municipio
self.addParameter(QgsProcessingParameterVectorDestination('pob_total', 'Archivo vectorial de salida con la población total del municipio'))
#Población atendida por clínicas
self.addParameter(QgsProcessingParameterVectorDestination('pob_atendida', 'Archivo vectorial de salida con la población atendida por las clínicas'))
#Población asignada a las clínicas
self.addParameter(QgsProcessingParameterVectorDestination('pob_asignada', 'Archivo vectorial de salida con la población asignada a las clínicas'))
'clinicas': capa vectorial de puntos con la ubicación de las clínicas;optional=Falseindica que este parámetro es obligatorio, el usuario no puede omitirlo.'municipio': capa vectorial de polígono con el límite municipal, usada más adelante para "recortar" los resultados intermedios.'poblacion': capa ráster con la distribución de población por celda, sobre la cual se calcularán las estadísticas de zona.'pob_total','pob_atendida','pob_asignada': los 3 archivos vectoriales de salida donde se guardará cada resultado.
Sección 4 · Pasos 1 y 2: generar el buffer y los polígonos de Voronoi
outputs = {}
results = {}
feedback = QgsProcessingMultiStepFeedback(7, model_feedback)
#Paso 1. Generar buffer a partir de las clínicas
alg_params = {
'INPUT':parameters['clinicas'],
'DISTANCE':5000,
'DISSOLVE':True,
'OUTPUT':'TEMPORARY_OUTPUT'
}
outputs['buffer'] = processing.run('native:buffer', alg_params, context=context, feedback=feedback, is_child_algorithm=True)
feedback.setCurrentStep(1)
if feedback.isCanceled():
return {}
#Paso 2. Polígonos de Voronoi a partir de las clínicas
alg_params = {
'INPUT':parameters['clinicas'],
'BUFFER':50,
'OUTPUT':'TEMPORARY_OUTPUT'
}
outputs['voronoi'] = processing.run('native:voronoipolygons', alg_params, context=context, feedback=feedback, is_child_algorithm=True)
feedback.setCurrentStep(2)
if feedback.isCanceled():
return {}
feedback = QgsProcessingMultiStepFeedback(7, model_feedback): declara los 7 pasos totales del modelo.'DISTANCE': 5000: crea un radio de 5,000 metros alrededor de cada clínica, representando su área de servicio.'DISSOLVE': True: fusiona todos los círculos de buffer en un solo polígono, para que zonas cubiertas por varias clínicas no se cuenten duplicadas al calcular la población atendida.outputs['buffer'] = processing.run('native:buffer', ...): ejecuta la herramienta nativa de buffer y guarda el resultado (aún sin recortar al municipio).'BUFFER': 50(en el paso 2): un pequeño margen adicional que la herramienta de Voronoi usa para extender los polígonos generados más allá del área estrictamente cubierta por los puntos.outputs['voronoi'] = processing.run('native:voronoipolygons', ...): genera un polígono de Voronoi por cada clínica; cada polígono representa el área más cercana a esa clínica que a cualquier otra.feedback.setCurrentStep(1)/feedback.setCurrentStep(2): avanzan la barra de progreso después de cada uno de estos dos primeros pasos.
Sección 5 · Pasos 3 y 4: recortar ambas capas al límite municipal
#Paso 3. Recortar el buffer al límite municipal
alg_params = {
'INPUT':outputs['buffer']['OUTPUT'],
'OVERLAY':parameters['municipio'],
'OUTPUT':'TEMPORARY_OUTPUT'
}
outputs['buffer_cortado'] = processing.run('native:clip', alg_params, context=context, feedback=feedback, is_child_algorithm=True)
feedback.setCurrentStep(3)
if feedback.isCanceled():
return {}
#Paso 4. Recortar lo polígonos de Voronoi al límite municipal
alg_params = {
'INPUT':outputs['voronoi']['OUTPUT'],
'OVERLAY':parameters['municipio'],
'OUTPUT':'TEMPORARY_OUTPUT'
}
outputs['voronoi_cortado'] = processing.run('native:clip', alg_params, context=context, feedback=feedback, is_child_algorithm=True)
feedback.setCurrentStep(4)
if feedback.isCanceled():
return {}
'INPUT': outputs['buffer']['OUTPUT']: usa como entrada el buffer generado en el paso 1, no la capa de clínicas original.'OVERLAY': parameters['municipio']: define el límite municipal como la capa que "recorta" al buffer, eliminando cualquier parte que quede fuera del municipio.outputs['buffer_cortado'] = processing.run('native:clip', ...): ejecuta el recorte y guarda el resultado, que se usará en el paso 6.- El segundo bloque repite exactamente el mismo patrón, pero recortando los polígonos de Voronoi (paso 2) en vez del buffer, guardando el resultado en
outputs['voronoi_cortado']para usarlo en el paso 7.
Sección 6 · Pasos 5, 6 y 7: calcular las 3 estadísticas de zona
#Paso 5. Estadísticas de zona para la población total
alg_params = {
'INPUT':parameters['municipio'],
'INPUT_RASTER':parameters['poblacion'],
'COLUMN_PREFIX':'Pob_',
'STATISTICS':[1],
'OUTPUT':parameters['pob_total']
}
outputs['pob_total'] = processing.run('native:zonalstatisticsfb', alg_params, context=context, feedback=feedback, is_child_algorithm=True)
feedback.setCurrentStep(5)
if feedback.isCanceled():
return {}
#Paso 6. Estadísticas de zona para la poblacion atendida
alg_params = {
'INPUT':outputs['buffer_cortado']['OUTPUT'],
'INPUT_RASTER':parameters['poblacion'],
'COLUMN_PREFIX':'Pob_',
'STATISTICS':[1],
'OUTPUT':parameters['pob_atendida']
}
outputs['pob_atendida'] = processing.run('native:zonalstatisticsfb', alg_params, context=context, feedback=feedback, is_child_algorithm=True)
feedback.setCurrentStep(6)
if feedback.isCanceled():
return {}
#Paso 7. Estadísticas de zona para la poblacion asignada
alg_params = {
'INPUT':outputs['voronoi_cortado']['OUTPUT'],
'INPUT_RASTER':parameters['poblacion'],
'COLUMN_PREFIX':'Pob_',
'STATISTICS':[1],
'OUTPUT':parameters['pob_asignada']
}
outputs['pob_asignada'] = processing.run('native:zonalstatisticsfb', alg_params, context=context, feedback=feedback, is_child_algorithm=True)
'INPUT_RASTER': parameters['poblacion']: en los 3 pasos se usa la misma capa ráster de población como fuente de los valores a resumir; lo que cambia es la capa vectorial de polígonos ('INPUT') sobre la que se calculan las estadísticas.'COLUMN_PREFIX': 'Pob_': todas las columnas nuevas que se agreguen a la tabla de atributos comenzarán con este prefijo (por ejemplo,Pob_sum).'STATISTICS': [1]: indica qué estadística calcular; el valor1corresponde a la suma de los valores del ráster dentro de cada polígono (es decir, la población total de esa zona).- Paso 5: usa como polígono el
'municipio'completo → obtiene la población total del municipio. - Paso 6: usa
outputs['buffer_cortado']['OUTPUT']→ obtiene la población atendida (dentro del radio de 5 km de alguna clínica, recortado al municipio). - Paso 7: usa
outputs['voronoi_cortado']['OUTPUT']→ obtiene la población asignada a cada clínica individual (cada polígono de Voronoi es el área más cercana a una clínica en particular). - Nota que el paso 7 no llama a
feedback.setCurrentStep(7)ni revisaisCanceled(), ya que es el último paso y el modelo continúa directamente hacia el bloque final.
Sección 7 · Entregar los 3 resultados finales
#Asignación de los resultados finales al diccionario results
results['pob_total'] = outputs['pob_total']['OUTPUT']
results['pob_atendida'] = outputs['pob_atendida']['OUTPUT']
results['pob_asiganda'] = outputs['pob_asignada']['OUTPUT']
print('Diccionario de outputs:')
print(outputs)
print('Diccionario de results:')
print(results)
return results
results['pob_total'] = outputs['pob_total']['OUTPUT']: copia la ruta del archivo de población total al diccionario de resultados finales.results['pob_atendida'] = outputs['pob_atendida']['OUTPUT']: copia la ruta del archivo de población atendida.results['pob_asiganda'] = outputs['pob_asignada']['OUTPUT']: copia la ruta del archivo de población asignada. Nota que la llave tiene una pequeña errata de escritura ('pob_asiganda'en vez de'pob_asignada'): Python no reporta ningún error por esto, ya que ambas son simplemente cadenas de texto distintas usadas como llaves de diccionario, pero es un buen ejemplo de por qué revisar cuidadosamente los nombres al programar.print(...): estas líneas imprimen ambos diccionarios en la consola de Python de QGIS, útiles para depurar y confirmar que los resultados se generaron correctamente.return results: entrega los 3 resultados finales a QGIS, cerrando la ejecución de la herramienta.