City Accessibility — Learn how to Attain Defibrillators on Time | by Milan Janosov | Oct, 2023


Picture by the writer.

On this piece, I mix earlier work on city accessibility or walkability with open-source information on the placement of public defibrillator units. Moreover, I incorporate world inhabitants information and Uber’s H3 grid system to estimate the share of the inhabitants inside affordable attain to any system inside Budapest and Vienna.

The foundation of city accessibility, or walkability, lies in a graph-based computation measuring the Euclidean distance (remodeling it into strolling minutes, assuming fixed velocity and no visitors jams and obstacles). The outcomes of such analyses can inform us how simple it’s to achieve particular kinds of facilities from each single location inside the metropolis. To be extra exact, from each single node inside the metropolis’s highway community, however because of a lot of highway crossings, this approximation is generally negligible.

On this present case research, I concentrate on one specific sort of Level of Curiosity (POI): the placement of defibrillator units. Whereas the Austrian Authorities’s Open Information Portal shares official data on this, in Hungary, I might solely receive a less-then-half protection crowd-sourced information set — which, hopefully, will later develop each in absolute dimension and information protection.

Within the first part of my article, I’ll create the accessibility map for every metropolis, visualizing the time wanted to achieve the closest defibrillator items inside a variety of two.5km at a working velocity of 15km/h. Then, I’ll break up the cities into hexagon grids utilizing Uber’s H3 library to compute the typical defibrillator-accessibility time for every grid cell. I additionally estimate the inhabitants degree at every hexagon cell following my earlier article. Lastly, I mix these and compute the fraction of the inhabitants reachable as a operate of reachability (working) time.

As a disclaimer, I need to emphasize that I’m not a skilled medical skilled by any means — and I don’t intend to take a stand on the significance of defibrillator units in comparison with different technique of life assist. Nevertheless, constructing on frequent sense and concrete planning ideas, I assume that the simpler it’s to achieve such units, the higher.

As all the time, I like to start out by exploring the information varieties I take advantage of. First, I’ll gather the executive boundaries of the cities I research in — Budapest, Hungary, and Vienna, Austria.

Then, constructing on a earlier article of mine on how one can course of rasterized inhabitants information, I add city-level inhabitants data from the WorldPop hub. Lastly, I incorporate official governmental information on defibrillator units in Vienna and my very own web-scraped model of the identical, although crowded sources and intrinsically incomplete, for Budapest.

1.1. Administrative boundaries

First, I question the admin boundaries of Budapest and Vienna from OpenStreetMap utilizing the OSMNx library:

import osmnx as ox # model: 1.0.1
import matplotlib.pyplot as plt # model: 3.7.1

admin = {}
cities = ['Budapest', 'Vienna']
f, ax = plt.subplots(1,2, figsize = (15,5))

# visualize the admin boundaries
for idx, metropolis in enumerate(cities):
admin[city] = ox.geocode_to_gdf(metropolis)
admin[city].plot(ax=ax[idx],shade='none',edgecolor= 'okay', linewidth = 2) ax[idx].set_title(metropolis, fontsize = 16)

The results of this code block:

Determine 1. The admin boundaries of Budapest and Vienna. Picture by the writer.

1.2. Inhabitants information

Second, following the steps on this article, I created the inhabitants grid in vector information format for each cities, constructing on the WorldPop on-line Demographic Database. With out repeating the steps, I simply learn within the output information of that course of containing inhabitants data for these cities.

Additionally, to make issues look good, I created a colormap from the colour of 2022, Very Peri, utilizing Matplotlib and a fast script from ChatGPT.

import matplotlib.pyplot as plt
from matplotlib.colours import LinearSegmentedColormap

very_peri = '#8C6BF3'
second_color = '#6BAB55'

colours = [second_color, very_peri ]
n_bins = 100
cmap_name = "VeryPeri"
colormap = LinearSegmentedColormap.from_list(cmap_name, colours, N=n_bins)

import geopandas as gpd # model: 0.9.0

demographics = {}
f, ax = plt.subplots(1,2, figsize = (15,5))

for idx, metropolis in enumerate(cities):
demographics[city] = gpd.read_file(metropolis.decrease() +
'_population_grid.geojson')[['population', 'geometry']]
admin[city].plot(ax=ax[idx], shade = 'none', edgecolor = 'okay',
linewidth = 3)
demographics[city].plot(column = 'inhabitants', cmap = colormap,
ax=ax[idx], alpha = 0.9, markersize = 0.25)
ax[idx].set_title(metropolis)
ax[idx].set_title('Inhabitants densityn in ' + metropolis, fontsize = 16)
ax[idx].axis('off')

The results of this code block:

Determine 2. Inhabitants density maps based mostly on WordPop information. Picture by the writer.

1.3. Defibrillator places

Third, I collected locational information on the obtainable defibrillators in each cities.

For Vienna, I downloaded this information set from the official open data portal of the Austrian government containing the purpose location of 1044 items:

Whereas such an official open information portal doesn’t exist in Budapest/Hungary, the Hungarian Nationwide Coronary heart Basis runs a crowd-sourced website the place operators can replace the placement of their defibrillator items. Their country-wide database consists of 677 items; nonetheless, their disclaimer says they learn about not less than one thousand items working within the nation — and are ready for his or her house owners to add them. With a easy net crawler, I downloaded the placement of every of the 677 registered items and filtered the information set right down to these in Budapest, leading to a set of 148 items.

# parse the information for every metropolis
gdf_units= {}

gdf_units['Vienna'] = gpd.read_file('DEFIBRILLATOROGD')
gdf_units['Budapest'] = gpd.read_file('budapest_defibrillator.geojson')

for metropolis in cities:
gdf_units[city] = gpd.overlay(gdf_units[city], admin[city])

# visualize the items
f, ax = plt.subplots(1,2, figsize = (15,5))

for idx, metropolis in enumerate(cities):
admin[city].plot(ax=ax[idx],shade='none',edgecolor= 'okay', linewidth = 3)
gdf_units[city].plot( ax=ax[idx], alpha = 0.9, shade = very_peri,
markersize = 6.0)
ax[idx].set_title('Areas of defibrillatorndevices in ' + metropolis,
fontsize = 16)
ax[idx].axis('off')

The results of this code block:

Determine 3. Defibrillator places in Budapest and Vienna. Picture by the writer.

Subsequent, I wrapped up this nice article written by Nick Jones in 2018 on how one can compute pedestrian accessibility:

import os
import pandana # model: 0.6
import pandas as pd # model: 1.4.2
import numpy as np # model: 1.22.4
from shapely.geometry import Level # model: 1.7.1
from pandana.loaders import osm

def get_city_accessibility(admin, POIs):

# walkability parameters
walkingspeed_kmh = 15
walkingspeed_mm = walkingspeed_kmh * 1000 / 60
distance = 2500

# bounding field as an inventory of llcrnrlat, llcrnrlng, urcrnrlat, urcrnrlng
minx, miny, maxx, maxy = admin.bounds.T[0].to_list()
bbox = [miny, minx, maxy, maxx]

# setting the enter params, going for the closest POI
num_pois = 1
num_categories = 1
bbox_string = '_'.be a part of([str(x) for x in bbox])
net_filename = 'information/network_{}.h5'.format(bbox_string)
if not os.path.exists('information'): os.makedirs('information')

# precomputing nework distances

if os.path.isfile(net_filename):
# if a road community file already exists, simply load the dataset from that
community = pandana.community.Community.from_hdf5(net_filename)
methodology = 'loaded from HDF5'
else:
# in any other case, question the OSM API for the road community inside the specified bounding field
community = osm.pdna_network_from_bbox(bbox[0], bbox[1], bbox[2], bbox[3])
methodology = 'downloaded from OSM'

# establish nodes which are linked to fewer than some threshold of different nodes inside a given distance
lcn = community.low_connectivity_nodes(impedance=1000, rely=10, imp_name='distance')
community.save_hdf5(net_filename, rm_nodes=lcn) #take away low-connectivity nodes and save to h5

# precomputes the vary queries (the reachable nodes inside this most distance)
# so, so long as you employ a smaller distance, cached outcomes will likely be used
community.precompute(distance + 1)

# compute accessibilities on POIs
pois = POIs.copy()
pois['lon'] = pois.geometry.apply(lambda g: g.x)
pois['lat'] = pois.geometry.apply(lambda g: g.y)
pois = pois.drop(columns = ['geometry'])
community.init_pois(num_categories=num_categories, max_dist=distance, max_pois=num_pois)

community.set_pois(class='all', x_col=pois['lon'], y_col=pois['lat'])

# searches for the n nearest facilities (of all kinds) to every node within the community
all_access = community.nearest_pois(distance=distance, class='all', num_pois=num_pois)

# remodel the outcomes right into a geodataframe
nodes = community.nodes_df
nodes_acc = nodes.merge(all_access[[1]], left_index = True, right_index = True).rename(columns = {1 : 'distance'})
nodes_acc['time'] = nodes_acc.distance / walkingspeed_mm
xs = listing(nodes_acc.x)
ys = listing(nodes_acc.y)
nodes_acc['geometry'] = [Point(xs[i], ys[i]) for i in vary(len(xs))]
nodes_acc = gpd.GeoDataFrame(nodes_acc)
nodes_acc = gpd.overlay(nodes_acc, admin)

nodes_acc[['time', 'geometry']].to_file(metropolis + '_accessibility.geojson', driver = 'GeoJSON')

return nodes_acc[['time', 'geometry']]

accessibilities = {}
for metropolis in cities:
accessibilities[city] = get_city_accessibility(admin[city], gdf_units[city])

for metropolis in cities:
print('Variety of highway community nodes in ' +
metropolis + ': ' + str(len(accessibilities[city])))

This code block outputs the variety of highway community nodes in Budapest (116,056) and in Vienna (148,212).

Now visualize the accessibility maps:

for metropolis in cities:
f, ax = plt.subplots(1,1,figsize=(15,8))
admin[city].plot(ax=ax, shade = 'okay', edgecolor = 'okay', linewidth = 3)
accessibilities[city].plot(column = 'time', cmap = 'RdYlGn_r',
legend = True, ax = ax, markersize = 2, alpha = 0.5)
ax.set_title('Defibrillator accessibility in minutesn' + metropolis,
pad = 40, fontsize = 24)
ax.axis('off')

This code block outputs the next figures:

Determine 4. Defibrillator accessibility in minutes in Budapest. Picture by the writer.
Determine 5. Defibrillator accessibility in minutes in Vienna. Picture by the writer.

At this level, I’ve each the inhabitants and the accessibility information; I simply must carry them collectively. The one trick is that their spatial items differ:

  • Accessibility is measured and connected to every node inside the highway community of every metropolis
  • Inhabitants information is derived from a raster grid, now described by the POI of every raster grid’s centroid

Whereas rehabilitating the unique raster grid could also be an possibility, within the hope of a extra pronounced universality (and including a little bit of my private style), I now map these two kinds of level information units into the H3 grid system of Uber for many who haven’t used it earlier than, for now, its sufficient to know that it’s a chic, environment friendly spacial indexing system utilizing hexagon tiles. And for extra studying, hit this hyperlink!

3.1. Creating H3 cells

First, put collectively a operate that splits a metropolis into hexagons at any given decision:

import geopandas as gpd
import h3 # model: 3.7.3
from shapely.geometry import Polygon # model: 1.7.1
import numpy as np

def split_admin_boundary_to_hexagons(admin_gdf, decision):
coords = listing(admin_gdf.geometry.to_list()[0].exterior.coords)
admin_geojson = {"sort": "Polygon", "coordinates": [coords]}
hexagons = h3.polyfill(admin_geojson, decision,
geo_json_conformant=True)
hexagon_geometries = {hex_id : Polygon(h3.h3_to_geo_boundary(hex_id,
geo_json=True)) for hex_id in hexagons}
return gpd.GeoDataFrame(hexagon_geometries.objects(), columns = ['hex_id', 'geometry'])

decision = 8
hexagons_gdf = split_admin_boundary_to_hexagons(admin[city], decision)
hexagons_gdf.plot()

The results of this code block:

Determine 6. The H3 hexagon break up of Vienna on the decision of 8. Picture by the writer.

Now, see a number of completely different resolutions:

for decision in [7,8,9]:

admin_h3 = {}
for metropolis in cities:
admin_h3[city] = split_admin_boundary_to_hexagons(admin[city], decision)

f, ax = plt.subplots(1,2, figsize = (15,5))

for idx, metropolis in enumerate(cities):
admin[city].plot(ax=ax[idx], shade = 'none', edgecolor = 'okay',
linewidth = 3)
admin_h3[city].plot( ax=ax[idx], alpha = 0.8, edgecolor = 'okay',
shade = 'none')
ax[idx].set_title(metropolis + ' (decision = '+str(decision)+')',
fontsize = 14)
ax[idx].axis('off')

The results of this code block:

Determine 7. H3 hexagon splitting of Budapest and Vienna at completely different resolutions. Picture by the writer.

Let’s hold decision 9!

3.2. Map values into h3 cells

Now, I’ve each our cities in a hexagon grid format. Subsequent, I shall map the inhabitants and accessibility information into the hexagon cells based mostly on which grid cells every level geometry falls into. For this, the sjoin operate of GeoPandasa, doing a pleasant spatial joint, is an effective selection.

Moreover, as we’ve greater than 100k highway community nodes in every metropolis and hundreds of inhabitants grid centroids, most definitely, there will likely be a number of POIs mapped into every hexagon grid cell. Subsequently, aggregation will likely be wanted. Because the inhabitants is an additive amount, I’ll mixture inhabitants ranges inside the similar hexagon by summing them up. Nevertheless, accessibility isn’t intensive, so I’d as an alternative compute the typical defibrillator accessibility time for every tile.

demographics_h3 = {}
accessibility_h3 = {}

for metropolis in cities:

# do the spatial be a part of, mixture on the inhabitants degree of every
# hexagon, after which map these inhabitants values to the grid ids
demographics_dict = gpd.sjoin(admin_h3[city], demographics[city]).groupby(by = 'hex_id').sum('inhabitants').to_dict()['population']
demographics_h3[city] = admin_h3[city].copy()
demographics_h3[city]['population'] = demographics_h3[city].hex_id.map(demographics_dict)

# do the spatial be a part of, mixture on the inhabitants degree by averaging
# accessiblity occasions inside every hexagon, after which map these time rating # to the grid ids
accessibility_dict = gpd.sjoin(admin_h3[city], accessibilities[city]).groupby(by = 'hex_id').imply('time').to_dict()['time']
accessibility_h3[city] = admin_h3[city].copy()
accessibility_h3[city]['time'] =
accessibility_h3[city].hex_id.map(accessibility_dict)

# now present the outcomes
f, ax = plt.subplots(2,1,figsize = (15,15))

demographics_h3[city].plot(column = 'inhabitants', legend = True,
cmap = colormap, ax=ax[0], alpha = 0.9, markersize = 0.25)
accessibility_h3[city].plot(column = 'time', cmap = 'RdYlGn_r',
legend = True, ax = ax[1])

ax[0].set_title('Inhabitants leveln in ' + metropolis, fontsize = 16)
ax[1].set_title('Defibrillator reachability timen in ' + metropolis,
fontsize = 16)

for ax_i in ax: ax_i.axis('off')

The outcomes of this code block are the next figures:

Determine 8. City options in Budapest. Picture by the writer.
Determine 9. City options in Vienna. Picture by the writer.

On this last step, I’ll estimate the fraction of the reachable inhabitants from the closest defibrillator unit inside a sure period of time. Right here, I nonetheless construct on the comparatively quick 15km/h working tempo and the two.5km distance restrict.

From the technical perspective, I merge the H3-level inhabitants and accessibility time information frames after which do a easy thresholding on the time dimension and a sum on the inhabitants dimension.

f, ax = plt.subplots(1,2, figsize = (15,5))

for idx, metropolis in enumerate(cities):

total_pop = demographics_h3[city].inhabitants.sum()
merged = demographics_h3[city].merge(accessibility_h3[city].drop(columns =
['geometry']), left_on = 'hex_id', right_on = 'hex_id')

time_thresholds = vary(10)
population_reached = [100*merged[merged.time<limit].inhabitants.sum()/total_pop for restrict in time_thresholds]

ax[idx].plot(time_thresholds, population_reached, linewidth = 3,
shade = very_peri)
ax[idx].set_xlabel('Reachability time (min)', fontsize = 14,
labelpad = 12)
ax[idx].set_ylabel('Fraction of inhabitants reached (%)', fontsize = 14, labelpad = 12)
ax[idx].set_xlim([0,10])
ax[idx].set_ylim([0,100])
ax[idx].set_title('Fraction of inhabitants vs defibrillatornaccessibility in ' + metropolis, pad = 20, fontsize = 16)

The results of this code block are the next figures:

Determine 10. The fraction of the inhabitants reachable from the closest defibrillator unit at a working tempo. Picture by the writer.

When decoding these outcomes, I wish to emphasize that, on the one hand, defibrillator accessibility is probably not instantly linked to heart-attack survival charge; judging that impact is past each my experience and this mission’s scope. Additionally, the information used for Budapest is knowingly incomplete and crowded sources, versus the official Austrian information supply.

After the disclaimers, what can we see? On the one hand, we see that in Budapest, about 75–80% of the inhabitants can get to a tool inside 10 minutes, whereas in Vienna, we attain almost full protection in round 6–7 minutes already. Moreover, we have to learn these time values fastidiously: if we occur to be at an unlucky incident, we have to get to the system, decide it up, return (making the journey time double of the reachability time), set up it, and so on. in a scenario the place each minute could also be a matter of life and dying.

So the takeaways, from a growth perspective, the takeaways are to make sure we’ve full information after which use the accessibility and inhabitants maps, mix them, analyze them, and construct on them when deploying new units and new places to maximise the efficient inhabitants reached.

Leave a Reply

Your email address will not be published. Required fields are marked *