Evaluar la igualdad verde urbana utilizando el portal de datos abiertos de Viena
Evaluating urban green equality using Vienna's open data portal
A pesar de sus muchas ventajas, el acceso a la naturaleza y a los espacios verdes se está haciendo cada vez más difícil en áreas altamente urbanizadas. Algunos temen que las comunidades desatendidas estén más expuestas a estos problemas. Aquí propongo una forma basada en datos para explorar esto.
En particular, planteo una pregunta sobre el desarrollo urbano que últimamente ha despertado interés en círculos profesionales y gobiernos locales, ahora conocida como igualdad verde. Este concepto se refiere a las disparidades en el acceso de las personas a los espacios verdes en diferentes partes de una ciudad en particular. Aquí, exploro su dimensión financiera y veo si existen relaciones claras entre el área verde disponible por habitante y el nivel económico de esa misma unidad urbana.
Exploraré dos resoluciones espaciales diferentes de la ciudad: distritos y distritos censales utilizando archivos de forma Esri proporcionados por el Portal de Datos Abiertos del Gobierno de Austria. También incorporaré datos estadísticos tabulares (población e ingresos) en las áreas administrativas georreferenciadas. Luego, superpondré las áreas administrativas con un conjunto de datos oficial de áreas verdes, registrando la ubicación de cada espacio verde en un formato geoespacial. A continuación, combinaré esta información y cuantificaré el tamaño total de espacio verde por habitante de cada distrito urbano. Finalmente, relacionaré el estado financiero de cada área, capturado por el ingreso neto anual, con la relación de espacio verde por habitante para ver si surgen patrones.
1. Fuente de datos
Echemos un vistazo al Portal de Datos Abiertos del Gobierno de Austria aquí.
Cuando estaba escribiendo este artículo, la traducción al inglés del sitio web no funcionaba realmente, así que en lugar de confiar en mis olvidados 12 años de clases de alemán, utilicé DeepL para navegar por las subpáginas y miles de conjuntos de datos.
Luego, recopilé un par de archivos de datos, tanto georreferenciados (shapefiles de Esri) como datos tabulares simples, que utilizaré para el análisis posterior. Los datos que recopilé:
- La IA generativa en la industria de la salud necesita una dosis de ...
- AnomalyGPT Detectando anomalías industriales utilizando LVLMs
- Anotación de imágenes de código cerrado frente a código abierto
Límites: los límites administrativos de las siguientes unidades espaciales en Viena:
- Los límites administrativos de Viena
- Los límites administrativos de los 23 distritos de Viena
- Los límites administrativos de los 250 distritos censales de Viena
Uso de la tierra: información sobre la ubicación de áreas verdes y áreas construidas:
- Cinturón Verde de Viena Ciudad de Viena que visualiza las áreas verdes existentes y dedicadas, compuestas por 1539 archivos de polígonos geoespaciales que encierran espacios verdes
Estadísticas: datos sobre población e ingresos correspondientes al nivel socioeconómico de un área:
- Población por distrito, registrada anualmente desde 2002 y almacenada dividida según grupos de edad de 5 años, género y nacionalidad original
- Población por distrito censal, registrada anualmente desde 2008 y almacenada dividida según tres grupos de edad irregulares, género y origen
- Ingreso neto promedio desde 2002 en los distritos de Viena, expresado en euros por empleado por año
Además, almacené los archivos de datos descargados en una carpeta local llamada data.
2. Exploración básica de los datos
2.1 Límites administrativos
Primero, leamos y visualicemos los diferentes archivos de forma que contienen cada nivel de límite administrativo para tener una mejor comprensión de la ciudad en cuestión:
carpeta = 'data'admin_ciudad = gpd.read_file(carpeta + '/LANDESGRENZEOGD')admin_distrito = gpd.read_file(carpeta + '/BEZIRKSGRENZEOGD')admin_censo = gpd.read_file(carpeta + '/ZAEHLBEZIRKOGD')display(admin_ciudad.head(1))display(admin_distrito.head(1))display(admin_censo.head(1))
Aquí cabe destacar que los nombres de las columnas BEZNR y ZBEZ corresponden al ID del distrito y al ID del distrito censal, respectivamente. Inesperadamente, se almacenan/analizan en formatos diferentes, numpy.float64 y str:
print(type(admin_district.BEZNR.iloc[0]))print(type(admin_census.ZBEZ.iloc[0]))pyth
Asegurándonos de que efectivamente tenemos 23 distritos y 250 distritos censales como lo afirmaba la documentación de los archivos de datos:
print(len(set(admin_district.BEZNR)))print(len(set(admin_census.ZBEZ)))
Ahora visualicemos los límites, primero de la ciudad, luego de sus distritos y finalmente de los distritos censales aún más pequeños.
f, ax = plt.subplots(1,3,figsize=(15,5))admin_city.plot(ax=ax[0], edgecolor = 'k', linewidth = 0.5, alpha = 0.9, cmap = 'Reds')admin_district.plot(ax=ax[1], edgecolor = 'k', linewidth = 0.5, alpha = 0.9, cmap = 'Blues')admin_census.plot(ax=ax[2], edgecolor = 'k', linewidth = 0.5, alpha = 0.9, cmap = 'Purples')ax[0].set_title('Límites de la ciudad')ax[1].set_title('Límites de los distritos')ax[2].set_title('Límites de los distritos censales')
Este código muestra las siguientes visualizaciones de Viena:

2.2 Áreas verdes
Ahora, también echémosle un vistazo a la distribución de las áreas verdes:
gdf_green = gpd.read_file(folder + '/GRUENFREIFLOGD_GRUENGEWOGD')display(gdf_green.head(3))
Aquí, uno puede notar que no hay una forma directa de vincular áreas verdes (por ejemplo, no se agregan identificadores de distrito) a los vecindarios, por lo que más adelante lo haremos manipulando las geometrías para encontrar superposiciones.
Ahora visualicemos esto:
f, ax = plt.subplots(1,1,figsize=(7,5))gdf_green.plot(ax=ax, edgecolor = 'k', linewidth = 0.5, alpha = 0.9, cmap = 'Greens')ax.set_title('Áreas verdes en Viena')
Este código muestra dónde se encuentran las áreas verdes dentro de Viena:

Podemos observar que los segmentos forestales todavía están dentro del límite administrativo, lo que implica que no todas las partes de la ciudad están urbanizadas y tienen una población significativa. Más adelante, volveremos a esto al evaluar el área verde per cápita.
2.3 Datos estadísticos: población, ingresos
Finalmente, echemos un vistazo a los archivos de datos estadísticos. La primera diferencia importante es que estos no tienen referencia geográfica, sino que son simplemente tablas csv:
df_pop_distr = pd.read_csv('vie-bez-pop-sex-age5-stk-ori-geo4-2002f.csv', sep = ';', encoding='unicode_escape', skiprows = 1)df_pop_cens = pd.read_csv('vie-zbz-pop-sex-agr3-stk-ori-geo2-2008f.csv', sep = ';', encoding='unicode_escape', skiprows = 1)df_inc_distr = pd.read_csv('vie-bez-biz-ecn-inc-sex-2002f.csv', sep = ';', encoding='unicode_escape', skiprows = 1)display(df_pop_distr.head(1))display(df_pop_cens.head(1))display(df_inc_distr.head(1))
3. Preprocesamiento de datos
3.1. Preparación de los archivos de datos estadísticos
La subsección anterior muestra que las tablas de datos estadísticos utilizan convenciones de nomenclatura diferentes: tienen identificadores DISTRICT_CODE y SUB_DISTRICT_CODE en lugar de BEZNR y ZBEZ. Sin embargo, después de leer la documentación de cada conjunto de datos, queda claro que es fácil transformar uno en otro, para lo cual presento dos funciones breves en la siguiente celda. Procesaré simultáneamente los datos a nivel de distritos y distritos censales.
Además, solo me interesarán los valores y puntos de datos agregados (más recientes) de la información estadística, como la población total en la última instantánea. Así que limpiemos estos archivos de datos y mantengamos las columnas que usaré más adelante.
# Estas funciones convierten los identificadores de distrito y distrito censal para que sean compatibles con los que se encuentran en los archivos de formasdef transform_district_id(x): return int(str(x)[1:3])def transform_census_district_id(x): return int(str(x)[1:5])# seleccionar el año más reciente del conjunto de datosdf_pop_distr_2 = df_pop_distr[df_pop_distr.REF_YEAR \ ==max(df_pop_distr.REF_YEAR)]df_pop_cens_2 = df_pop_cens[df_pop_cens.REF_YEAR \ ==max(df_pop_cens.REF_YEAR)]df_inc_distr_2 = df_inc_distr[df_inc_distr.REF_YEAR \ ==max(df_inc_distr.REF_YEAR)]# convertir identificadores de distritodf_pop_distr_2['district_id'] = \ df_pop_distr_2.DISTRICT_CODE.apply(transform_district_id)df_pop_cens_2['census_district_id'] = \ df_pop_cens_2.SUB_DISTRICT_CODE.apply(transform_census_district_id)df_inc_distr_2['district_id'] = \ df_inc_distr_2.DISTRICT_CODE.apply(transform_district_id)# valores de población agregadosdf_pop_distr_2 = df_pop_distr_2.groupby(by = 'district_id').sum()df_pop_distr_2['district_population'] = df_pop_distr_2.AUT + \ df_pop_distr_2.EEA + df_pop_distr_2.REU + df_pop_distr_2.TCNdf_pop_distr_2 = df_pop_distr_2[['district_population']]df_pop_cens_2 = df_pop_cens_2.groupby(by = 'census_district_id').sum()df_pop_cens_2['census_district_population'] = df_pop_cens_2.AUT \ + df_pop_cens_2.FORdf_pop_cens_2 = df_pop_cens_2[['census_district_population']]df_inc_distr_2['district_average_income'] = \ 1000*df_inc_distr_2[['INC_TOT_VALUE']]df_inc_distr_2 = \ df_inc_distr_2.set_index('district_id')[['district_average_income']]# mostrar las tablas finalesdisplay(df_pop_distr_2.head(3))display(df_pop_cens_2.head(3))display(df_inc_distr_2.head(3))# y unificar las convenciones de nomenclaturadistricto_admin['district_id'] = districto_admin.BEZNR.astype(int)censo_admin['census_district_id'] = censo_admin.ZBEZ.astype(int)print(len(set(censo_admin.ZBEZ)))
Verifique nuevamente los valores de población total calculados en los dos niveles de agregación:
print(sum(df_pop_distr_2.district_population))print(sum(df_pop_cens_2.census_district_population))
Estos dos deberían proporcionar el mismo resultado: 1931593 personas.
3.1. Preparando los archivos de datos geoespaciales
Ahora que hemos terminado con la preparación de datos esenciales de los archivos estadísticos, es hora de hacer coincidir los polígonos de áreas verdes con los polígonos de áreas administrativas. Luego, calculemos la cobertura total de áreas verdes de cada área administrativa. Además, agregaré la cobertura relativa de áreas verdes de cada área administrativa por curiosidad.
Para obtener áreas expresadas en unidades del sistema internacional, debemos cambiar a un llamado CRS local, que en el caso de Viena es EPSG:31282. Puede leer más sobre este tema, proyección de mapas y sistemas de referencia de coordenadas aquí y aquí.
# convirtiendo todos los GeoDataFrames en el CRS localdistricto_admin_2 = \ districto_admin[['district_id', 'geometry']].to_crs(31282)censo_admin_2 = \ censo_admin[['census_district_id', 'geometry']].to_crs(31282)gdf_verde_2 = gdf_verde.to_crs(31282)
Calcule el área de la unidad administrativa medida en unidades del sistema internacional:
districto_admin_2['area_admin'] = \ districto_admin_2.geometry.apply(lambda g: g.area)censo_admin_2['area_admin'] = \ censo_admin_2.geometry.apply(lambda g: g.area)display(districto_admin_2.head(1))display(censo_admin_2.head(1))
4. Calcular la proporción de área verde por habitante
4.1 Calcular la cobertura de área verde en cada unidad administrativa
Utilizaré la función de superposición de GeoPandas para superponer estas dos GeoDataFrames de límites administrativos con el GeoDataFrame que contiene los polígonos de área verde. Luego, calculo el área de cada sección de área verde que cae en diferentes regiones administrativas. A continuación, sumo estas áreas al nivel de cada área administrativa, tanto distritos como distritos censales. En el paso final, en cada unidad de resolución, agrego las áreas de las unidades administrativas previamente calculadas y calculo la proporción de área total a área verde para cada distrito y distrito censal.
gdf_green_mapped_distr = gpd.overlay(gdf_green_2, admin_district_2)gdf_green_mapped_distr['green_area'] = \ gdf_green_mapped_distr.geometry.apply(lambda g: g.area) gdf_green_mapped_distr = \ gdf_green_mapped_distr.groupby(by = 'district_id').sum()[['green_area']]gdf_green_mapped_distr = \ gpd.GeoDataFrame(admin_district_2.merge(gdf_green_mapped_distr, left_on = 'district_id', right_index = True))gdf_green_mapped_distr['green_ratio'] = \ gdf_green_mapped_distr.green_area / gdf_green_mapped_distr.admin_areagdf_green_mapped_distr.head(3)
gdf_green_mapped_cens = gpd.overlay(gdf_green_2, admin_census_2)gdf_green_mapped_cens['green_area'] = \ gdf_green_mapped_cens.geometry.apply(lambda g: g.area)gdf_green_mapped_cens = \ gdf_green_mapped_cens.groupby(by = 'census_district_id').sum()[['green_area']]gdf_green_mapped_cens = \ gpd.GeoDataFrame(admin_census_2.merge(gdf_green_mapped_cens, left_on = 'census_district_id', right_index = True))gdf_green_mapped_cens['green_ratio'] = gdf_green_mapped_cens.green_area / gdf_green_mapped_cens.admin_areagdf_green_mapped_cens.head(3)
¡Finalmente, visualice la proporción de área verde por distrito y distrito censal! Los resultados parecen tener mucho sentido, con un alto nivel de vegetación en las partes exteriores y mucho menor en las áreas centrales. Además, los 250 distritos censales muestran claramente una imagen más detallada y detallada de las características de los diferentes vecindarios, ofreciendo información más profunda y localizada para los planificadores urbanos. Por otro lado, la información a nivel de distrito, con diez veces menos unidades espaciales, muestra promedios generales.
f, ax = plt.subplots(1,2,figsize=(17,5))gdf_green_mapped_distr.plot(ax = ax[0], column = 'green_ratio', edgecolor = 'k', linewidth = 0.5, alpha = 0.9, legend = True, cmap = 'Greens')gdf_green_mapped_cens.plot(ax = ax[1], column = 'green_ratio', edgecolor = 'k', linewidth = 0.5, alpha = 0.9, legend = True, cmap = 'Greens')
Este bloque de código muestra los siguientes mapas:

4.2 Agregar información de población e ingresos para cada unidad administrativa
En el último paso de esta sección, mapearemos los datos estadísticos en áreas administrativas. Recordatorio: Tenemos datos de población tanto a nivel de distritos como a nivel de distritos censales. Sin embargo, solo pude encontrar ingresos (indicador de nivel socioeconómico) a nivel de distritos. Este es un compromiso habitual en la ciencia de datos geoespaciales. Mientras que una dimensión (vegetación) es mucho más informativa a una resolución más alta (distritos censales), las limitaciones de datos pueden obligarnos a utilizar la resolución más baja de todos modos.
display(admin_census_2.head(2))display(df_pop_cens_2.head(2))
gdf_pop_mapped_distr = admin_district_2.merge(df_pop_distr_2, \ left_on = 'district_id', right_index = True)gdf_pop_mapped_cens = admin_census_2.merge(df_pop_cens_2, \ left_on = 'census_district_id', right_index = True)gdf_inc_mapped_distr = admin_district_2.merge(df_inc_distr_2, \ left_on = 'district_id', right_index = True)f, ax = plt.subplots(1,3,figsize=(15,5))gdf_pop_mapped_distr.plot(column = 'district_population', ax=ax[0], \ edgecolor = 'k', linewidth = 0.5, alpha = 0.9, cmap = 'Blues')gdf_pop_mapped_cens.plot(column = 'census_district_population', ax=ax[1], \ edgecolor = 'k', linewidth = 0.5, alpha = 0.9, cmap = 'Blues')gdf_inc_mapped_distr.plot(column = 'district_average_income', ax=ax[2], \ edgecolor = 'k', linewidth = 0.5, alpha = 0.9, cmap = 'Purples')ax[0].set_title('district_population')ax[1].set_title('census_district_population')ax[2].set_title('district_average_incomee')
Este bloque de códigos resulta en la siguiente figura:

4.3. Cálculo del área verde por habitante
Ahora, resumamos lo que tenemos hasta ahora, todo integrado en archivos de formas adecuados correspondientes a los distritos y distritos censales de Viena:
En el nivel de los distritos, tenemos la proporción de área verde, la población y los datos de ingresos
En el nivel de los distritos censales, tenemos una proporción de área verde y datos de población
Para capturar la igualdad verde simplemente, combino la información sobre el tamaño absoluto del área verde y la población en los distritos y distritos censales y calculo la cantidad total de área verde por habitante.
Echemos un vistazo a nuestra entrada: cobertura verde y población:
# un gráfico para los distritos
f, ax = plt.subplots(1,2,figsize=(10,5))
gdf_green_mapped_distr.plot(ax=ax[0], column='green_ratio', edgecolor='k', linewidth=0.5, alpha=0.9, cmap='Greens')
gdf_pop_mapped_distr.plot(ax=ax[1], column='district_population', edgecolor='k', linewidth=0.5, alpha=0.9, cmap='Reds')
ax[0].set_title('green_ratio')
ax[1].set_title('district_population')
# un gráfico para los distritos censales
f, ax = plt.subplots(1,2,figsize=(10,5))
gdf_green_mapped_cens.plot(ax=ax[0], column='green_ratio', edgecolor='k', linewidth=0.5, alpha=0.9, cmap='Greens')
gdf_pop_mapped_cens.plot(ax=ax[1], column='census_district_population', edgecolor='k', linewidth=0.5, alpha=0.9, cmap='Reds')
ax[0].set_title('green_ratio')
ax[1].set_title('district_population')
Este bloque de códigos resulta en la siguiente figura:

Para calcular el área verde por habitante, primero fusionaré los marcos de datos de vegetación y población en los siguientes pasos. Lo haré a través del ejemplo de los distritos censales porque su mayor resolución espacial nos permite observar mejor los patrones (si los hay) que surgen. Asegurémonos de no dividir por cero y también sigamos el sentido común; eliminemos esas áreas que no tienen población.
gdf_green_pop_cens = gdf_green_mapped_cens.merge(gdf_pop_mapped_cens.drop(columns=['geometry', 'admin_area']), left_on='census_district_id', right_on='census_district_id')[['census_district_id', 'green_area', 'census_district_population', 'geometry']]
gdf_green_pop_cens['green_area_per_capita'] = gdf_green_pop_cens['green_area'] / gdf_green_pop_cens['census_district_population']
gdf_green_pop_cens = gdf_green_pop_cens[gdf_green_pop_cens['census_district_population'] > 0]
f, ax = plt.subplots(1,1,figsize=(10,7))
gdf_green_pop_cens.plot(column='green_area_per_capita', ax=ax, cmap='RdYlGn', edgecolor='k', linewidth=0.5)
admin_district.to_crs(31282).plot(ax=ax, color='none', edgecolor='k', linewidth=2.5)
Este bloque de códigos resulta en la siguiente figura:

Vamos a ajustar un poco la visualización:
f, ax = plt.subplots(1,1,figsize=(11,7))ax.set_title('Área verde per cápita en\nlos distritos censales de Viena', fontsize = 18, pad = 30)gdf_green_pop_cens.plot( column = 'green_area_per_capita', ax=ax, cmap = 'RdYlGn', edgecolor = 'k', linewidth = 0.5, legend=True, norm=matplotlib.colors.LogNorm(\ vmin=gdf_green_pop_cens.green_area_per_capita.min(), \ vmax=gdf_green_pop_cens.green_area_per_capita.max()), )admin_district.to_crs(31282).plot( ax=ax, color = 'none', edgecolor = 'k', linewidth = 2.5)
Este bloque de códigos resulta en la siguiente figura:

Y lo mismo para los distritos:
# calcular las puntuaciones de área verde per cápitagdf_green_pop_distr = \ gdf_green_mapped_distr.merge(gdf_pop_mapped_distr.drop(columns = \ ['geometry', 'admin_area']), left_on = 'district_id', right_on = \ 'district_id')[['district_id', 'green_area', 'district_population', \ 'geometry']]gdf_green_popdistr = \ gdf_green_pop_distr[gdf_green_pop_distr.district_population>0]gdf_green_pop_distr['green_area_per_capita'] = \ gdf_green_pop_distr['green_area'] / \ gdf_green_pop_distr['district_population']# visualizar el mapa a nivel de distritof, ax = plt.subplots(1,1,figsize=(10,8))ax.set_title('Área verde per cápita en los distritos de Viena', \ fontsize = 18, pad = 26)gdf_green_pop_distr.plot(column = 'green_area_per_capita', ax=ax, \ cmap = 'RdYlGn', edgecolor = 'k', linewidth = 0.5, legend=True, \ norm=matplotlib.colors.LogNorm(vmin=\ gdf_green_pop_cens.green_area_per_capita.min(), \ vmax=gdf_green_pop_cens.green_area_per_capita.max()), )admin_city.to_crs(31282).plot(ax=ax, \ color = 'none', edgecolor = 'k', linewidth = 2.5)
Este bloque de códigos resulta en la siguiente figura:

Aunque las tendencias significativas son claras — el borde exterior, más espacio verde para todos, el centro construido, invertido. Sin embargo, estos dos gráficos, especialmente el más detallado a nivel de distritos censales, muestran claramente una variación en la cantidad de espacio verde que las personas disfrutan en las diferentes áreas. Investigaciones adicionales e incorporación de fuentes de datos adicionales, por ejemplo, sobre el uso del suelo, podrían ayudar a explicar mejor por qué esas áreas tienen una mayor área verde o población. Por ahora, ¡disfrutemos de este mapa y esperemos que todos encuentren la cantidad adecuada de vegetación en su hogar!
# fusionar los datos de vegetación, población e ingresosgdf_district_green_pip_inc = \ gdf_green_pop_distr.merge(gdf_inc_mapped_distr.drop(columns = \ ['geometry']))
Visualizar la relación entre las dimensiones financieras y de vegetación:
f, ax = plt.subplots(1,1,figsize=(6,4))ax.plot(gdf_district_green_pip_inc.district_average_income, \ gdf_district_green_pip_inc.green_area_per_capita, 'o')ax.set_xscale('log')ax.set_yscale('log')ax.set_xlabel('district_average_income')ax.set_ylabel('green_area_per_capita')
El resultado de este bloque de código es el siguiente gráfico de dispersión:

A primera vista, el gráfico de dispersión no establece un caso fuerte para que las finanzas determinen el acceso de las personas a espacios verdes. Honestamente, estoy un poco sorprendido por estos resultados, sin embargo, a la luz de los esfuerzos conscientes y de larga data de Viena para verderar su ciudad, puede ser por qué no vemos ninguna tendencia importante aquí. Para confirmar, también verifiqué las correlaciones entre estas dos variables:
print(spearmanr(gdf_district_green_pip_inc.district_average_income, gdf_district_green_pip_inc.green_area_per_capita))print(pearsonr(gdf_district_green_pip_inc.district_average_income, gdf_district_green_pip_inc.green_area_per_capita))
Debido a la distribución con colas pesadas de los datos financieros, tomaría la correlación de Spearman (0.13) más en serio aquí, pero incluso la correlación de Pearson (0.30) implica una tendencia relativamente débil, en línea con mis observaciones anteriores.