3) Urban Network Analysis (UNA) Tools: Accessibility, Closest Facility and Service Area
We learned how to load and visualize layers in section 1, and learned how to create, diagnose and visualize network in section 2. In this section we wil learn how to use the UNA Acissibility tools. As we learned from the preivos two section, the following code:
loads the sidewalk, buildinbg entrances and subway entrances geometries.
Create a topological network
Create the graph objects.
This is all we need to start using the UNA tools in Madina.
[1]:
import madina as md
cambridge = md.Zonal()
#Loading sidewalks, buildings and subway geometries.
cambridge.load_layer('sidewalks', 'Cities/Cambridge/Data/sidewalks.geojson')
cambridge.load_layer('buildings', 'Cities/Cambridge/Data/building_entrances.geojson')
cambridge.load_layer('subway', 'Cities/Cambridge/Data/subway.geojson')
# Creating a network, and adding origins and destinations
cambridge.create_street_network(source_layer="sidewalks", node_snapping_tolerance=0.1)
cambridge.insert_node(label='origin', layer_name="subway")
cambridge.insert_node(label='destination', layer_name="buildings")
# Creating graphs
cambridge.create_graph()
We first need to import the UNA tools module:
[2]:
import madina.una.tools as una
We want to know how many building entrances or people can be reached from subway stations in a 5-minute (300m) walkshed.
una.accessibility() - Reach Index
Running the una.accessibility and giving it parameters reach=True, search_radius=300 will measure how many destinations (building entrances) are reachable from origins (subway). We see that the northen station could be reached by 106 building entrance, while the southern station could be reached by 112 building entrance.
The una.accessibility stores the reach result in a column in the origin layer if the parameter save_reach_as is set to a column name
[3]:
una.accessibility(
cambridge,
search_radius=300,
save_reach_as="reach_to_buildings"
)
cambridge.create_map(
[
{"layer": 'sidewalks', 'color': [125, 125, 125]},
{"layer": "subway", "radius": "reach_to_buildings", 'text':'reach_to_buildings', 'color': [255, 0, 0]},
]
)
Time spent: 0s [Done 2 of 2 origins (100.00%)]
[3]:
Setting the parameter destination_weight=’people’ enables us to weight the destinations (building entrances) by how many people actually live in these building. We could then see that 2,789 people could reach the northen station by walking a maximum of 300 meters. The southern station could be reached by 3,017 people.
[4]:
una.accessibility(
cambridge,
search_radius=300,
destination_weight='people',
save_reach_as="reach_to_people"
)
cambridge.create_map(
[
{"layer": 'sidewalks', 'color': [125, 125, 125]},
{"layer": "subway", "radius": "reach_to_people", 'text':'reach_to_people', 'color': [255, 0, 0]},
]
)
Time spent: 0s [Done 2 of 2 origins (100.00%)]
[4]:
una.service_area(): Finding catchment areas
Sometimes, we want to examine a section of our data in isolation. The service_area() function is a great introduction into that. The library internally uses a library called GeoPandas, where a layer is stored as a GeoDataFrame. GeoDataFrames are an extenstion to the well known Pandas library’s DataFrame. If you’re familiar with the Pandas Dataframe, you’ll be able to use all the functionality you’re familiar with. The only
difference is that a GeoDataFrame always has a geometry column, and supports a wide array of spatial operations.
The service_area() function returns:
destinations: a GeoDataFrame containing all the destinations that are reachable and covered by an origin’s service area.network_edges: a GeoDataframe containing all network segments inside the origin’s service area.scope_gdf: a Dataframe that contains the boundaries of the service area.
As discussed in section 1, create_map() could take either a layer name from the layers we loaded, or a gdf (i.e. GeoDataFrame).
[5]:
destinations, network_edges, scope_gdf = una.service_area(
cambridge,
search_radius=100,
)
cambridge.create_map(
layer_list=[
{"layer": 'sidewalks', 'opacity': 0.1},
{"layer": 'buildings', 'opacity': 0.1},
{"layer": 'subway'},
{"gdf": network_edges, "color": [0, 255, 0]},
{"gdf": destinations, "color": [255, 0, 0]},
{"gdf": scope_gdf, "color": [0, 0, 255], 'opacity': 0.10},
]
)
[5]:
let’s see the impact of imposing a turn penalty on the service area:
[6]:
cambridge.set_turn_parameters(
turn_penalty_amount=25,
turn_threshold_degree=45
)
destinations, network_edges, scope_gdf = una.service_area(
cambridge,
search_radius=100,
turn_penalty=True,
)
cambridge.create_map(
[
{"layer": 'sidewalks', 'opacity': 0.1},
{"layer": 'buildings', 'opacity': 0.1},
{"layer": 'subway'},
{"gdf": network_edges, "color": [0, 255, 0]},
{"gdf": destinations, "color": [255, 0, 0]},
{"gdf": scope_gdf, "color": [0, 0, 255], 'opacity': 0.10},
]
)
[6]:
We see each station’s service area getting smaller in response to imposing a penalty on turns.
Modifying attributes in layers
It is possible to make changes to layers by modifying existing values or adding new attributes. these new or modified attributes could be used as weights in UNA tools. Every object geometry in a layer has an identifier, and each layer once loaded is assigned an id attribute. We can visualize these ids
[7]:
cambridge.create_map(
[
{"layer": 'sidewalks', 'color': [125, 125, 125]},
{"layer": 'buildings', 'text':'id', 'color': [255, 0, 0]},
]
)
[7]:
We can give building entrances with ids 2 and 115 a new atttribute students and assign values 10 and 10, for building ids 2 and 115 respectively. We could do that by accessing the Geodataframe of the layer building, then using the function at(id_number, attribute_name) to set individual attributes.
For visualization, we could use the pandas’ loc to access specific rows/geometries by specifying a list of their ids. The next map shows building entrances with ids 2 and 115, and shows their students attribute which we’ve set for both to 10
[8]:
cambridge['buildings'].gdf.at[2, 'students'] = 10
cambridge['buildings'].gdf.at[115, 'students'] = 10
cambridge.create_map(
[
{"layer": 'sidewalks', 'color': [125, 125, 125]},
{"gdf": cambridge['buildings'].gdf.loc[[2, 115]], 'text':'students', 'color': [255, 0, 0]},
]
)
[8]:
From the map below, we now notice that 20 students can reach the northen station, and 10 students could reach he southern station on a 300 meter walk.
[9]:
una.accessibility(
cambridge,
search_radius=300,
destination_weight='students',
save_reach_as='reach_to_students'
)
cambridge.create_map(
[
{"layer": 'sidewalks', 'color': [125, 125, 125]},
{"layer": 'subway', "radius": "reach_to_students", 'text':'reach_to_students', 'color': [255, 0, 0]},
]
)
Time spent: 0s [Done 2 of 2 origins (100.00%)]
[9]:
una.accessibility() - Gravity Index
The gravity model of transportation is one of many distance decay functions. The aim of a distance decay function (e.g. gravity) is to quantify the inverse relationship between distance, and willingness to make trips. applying gravity peanilize further destinations by reducing their contribution, assume that they are less accissable. For this reason, a gravity index is always less than or equal to a reach index.
When factoring in a gravity decay, we notice that the northen station only attracts 16.88 students (As opposed to 20) and the southern station now attracts only 8.67 students (as opposed to 10).
[10]:
una.accessibility(
cambridge,
search_radius=300,
beta=0.001,
destination_weight='students',
save_gravity_as='gravity_to_students'
)
cambridge.create_map(
[
{"layer": 'sidewalks', 'color': [125, 125, 125]},
{"layer": 'subway', "radius": "gravity_to_students", 'text':'gravity_to_students', 'color': [255, 0, 0]},
]
)
Time spent: 0s [Done 2 of 2 origins (100.00%)]
[10]:
Comparing Reach and Gravity of People to Subway
We go back to using people as weight, and we again call the function una_accessibility(), this time, setting both reach and gravity to True. The map below shows the two stations with their grqavity score. You can hover each station to see more information. The northern station has a reach index of 2,789 but a gravity index of only 2,361.54, while the southern station has a reach index of 3,017 and a gravity index of only 2,606.81. Notice how you can save both metrics at the same time
when providing parameters: save_reach_as, save_gravity_as
[11]:
una.accessibility(
cambridge,
search_radius=300,
beta=0.001,
destination_weight='people',
save_reach_as='reach_to_people',
save_gravity_as='gravity_to_people'
)
cambridge.create_map(
[
{"layer": 'sidewalks', 'color': [125, 125, 125]},
{"layer": 'subway', "radius": "gravity_to_people", 'text':'gravity_to_people', 'color': [255, 0, 0]},
]
)
Time spent: 0s [Done 2 of 2 origins (100.00%)]
[11]:
una.accessibility(): Closets Facilities and Distances
When using una_accessibility(), there is potential for overlap between destinations assigned to each origin, so a destination would be double counted in an origin’s accissability indices. The setting the parameterclosest_facility=True assigns each destination (in this case, building entrance) to one single origin (a subway station in this case)
[12]:
una.accessibility(
cambridge,
search_radius=300,
beta=0.001,
destination_weight='people',
closest_facility=True,
save_reach_as='reach_to_closest_people',
save_gravity_as='gravity_to_closest_people',
save_closest_facility_as='closest_subway',
save_closest_facility_distance_as='distance_to_closest_subway',
)
Time spent: 0s [Done 2 of 2 origins (100.00%)]
We can see how destinations are assigned to their closest origin
[13]:
cambridge.create_map(
[
{"layer": 'sidewalks', 'color': [125, 125, 125]},
{"layer": 'buildings',"text": "closest_subway", "color_by_attribute": "closest_subway", "color_method": "categorical", "color": {0:[125, 0, 125], 1:[0, 125, 125]}},
{"layer": 'subway', 'color': [255, 255, 0], 'text':'id'}
]
)
[13]:
And we can also see distances to closest destination:
[14]:
cambridge.create_map(
[
{"layer": 'sidewalks', 'color': [125, 125, 125]},
{"layer": 'buildings',"text": "distance_to_closest_subway", "color_by_attribute": "closest_subway", "color_method": "categorical"},
]
)
[14]:
When destinations are assigned to their closest facility, we can see how that impacts reach and gravity, as origins are splitting destinations between them and a destination is only reachable by its closest origin. For reach:
[15]:
cambridge.create_map(
[
{"layer": 'sidewalks', 'color': [125, 125, 125]},
{"layer": 'subway', "radius": "reach_to_closest_people", 'text':'reach_to_closest_people', 'color': [255, 0, 0]},
]
)
[15]:
and for gravity:
[16]:
cambridge.create_map(
layer_list=[
{"layer": 'sidewalks', 'color': [125, 125, 125]},
{"layer": 'subway', "radius": "gravity_to_closest_people", 'text':'gravity_to_closest_people', 'color': [255, 0, 0]},
]
)
[16]:
Compare how reach and gravity vary when using accissibility and closest facility
Function | una.accessibility() | una.closest_facility() | ||
Station | Reach | Graviy | Reach | Gravity |
Northren | 2,789.00 | 2.361.54 | 1,265.00 | 1,120.92 |
Southren | 3,017.00 | 2,606.81 | 1,914.00 | 1,716.57 |
The Reverse: Reach and Gravity of Subway to People
To examine the different interpretation of accessibility indices based on what we define as origins and destinations, lets flip our origins from subway to buildings' and destinations frombuildingstosubway`.
In order to do this, we need to first clear up the origins and destination nodes we created earlier, bur we could still use the same neteowk edges (sidewalks) as before, since changing the origins and destinations will not impact the underlying network segments and intersections. Calling the function clear_nodes() removes existing oeigins and destinations. We then need to insert origins and destinations, then create a graph object, just like before.
[17]:
cambridge.clear_nodes()
cambridge.insert_node(layer_name="buildings", label='origin')
cambridge.insert_node(layer_name="subway", label='destination')
cambridge.create_graph()
The reach index shown below, shows how many subway stations could be reached from each building enterance, by walking 300 meters. Most building entrances can access two stations, but buildings on the phrepherey, could only access one of the two stations within a 300 meter walk,
[18]:
una.accessibility(
cambridge,
search_radius=300,
save_reach_as='reach_to_subway'
)
cambridge.create_map(
[
{"layer": 'sidewalks', 'color': [125, 125, 125]},
{"layer": 'buildings', "color_by_attribute": "reach_to_subway", 'color_method': 'gradient', 'text':'reach_to_subway'}
]
)
Time spent: 0s [Done 118 of 118 origins (100.00%)]
[18]:
We could add an attriute called daily_trains to each subway station, measure and vosialize the reach accissibility metric. Notice that the subway stations are in yellow and labeled with the new daily_trains attribute
[19]:
cambridge.layers['subway'].gdf.at[0, 'daily_trains'] = 20
cambridge.layers['subway'].gdf.at[1, 'daily_trains'] = 50
una.accessibility(
cambridge,
search_radius=300,
destination_weight='daily_trains',
save_reach_as='reach_to_daily_trains'
)
cambridge.create_map(
[
{"layer": 'sidewalks', 'color': [125, 125, 125]},
{"layer": 'buildings', "color_by_attribute": "reach_to_daily_trains", 'color_method': 'gradient', 'text':'reach_to_daily_trains'},
{"layer": 'subway', 'color': [255, 255, 0], 'text':'daily_trains'}
]
)
Time spent: 1s [Done 118 of 118 origins (100.00%)]
[19]:
We notice some buildings now have access to either 20, 50 or 70 daily trains, depending on what stations they can access within a 300 meter walk.
As for the gravity index, the interpretation of what these values mean is sensitive to the `beta`` parameter which could translate to the “area’s willingness to walk”. living in a building with a high gravity index, means that you have more subway options that are close and could easily be reached on foot. A low gravity index, means that you live in a building with fewer subway options that are further out.
[20]:
una.accessibility(
cambridge,
search_radius=300,
beta=0.004,
save_gravity_as='gravity_to_subway'
)
cambridge.create_map(
layer_list=[
{"layer": 'sidewalks', 'color': [135, 125, 125]},
{"layer": 'buildings', "color_by_attribute": "gravity_to_subway", 'color_method':'gradient', 'text': 'gravity_to_subway'},
]
)
Time spent: 0s [Done 118 of 118 origins (100.00%)]
[20]:
When setting destination_weight='daily_trains', each building is assigned a gravity-adjusted number of daily train based on both, how many stations this building can access within a 300 meter walk, and how far are these stations.
[21]:
una.accessibility(
cambridge,
search_radius=300,
beta=0.004,
destination_weight='daily_trains',
save_gravity_as='gravity_to_daily_trains'
)
cambridge.create_map(
[
{"layer": 'sidewalks', 'color': [135, 125, 125]},
{"layer": 'buildings', "color_by_attribute": "gravity_to_daily_trains", 'color_method':'gradient', 'text': 'gravity_to_daily_trains'},
]
)
Time spent: 1s [Done 118 of 118 origins (100.00%)]
[21]:
KNN Access to trains: acccessibility to suffecient destinations.
K Nearest Neighbor Access (KNN Access) is another accessibility metric that measures if a suffecient level of access is provided. This could be done by specifying how many destinations are considered suffecient, and the relative weight for each additional destinations. In this example, if we assumed it would be suffecient for all origins (buildings) to be able to reach two subway stations, and if we assumed that access to the first subway station adds 75% of the access score, and access to the
second subway station adds the remaining 25%, then we need to set the parameter knn_weights=[0.75, 0.25]
[22]:
una.accessibility(
cambridge,
search_radius=300,
knn_weights=[0.75, 0.25],
save_knn_access_as='knn_access_to_subway'
)
cambridge.create_map(
[
{"layer": 'sidewalks', 'color': [135, 125, 125]},
{"layer": 'buildings', "color_by_attribute": "knn_access_to_subway", 'color_method':'gradient', 'text': 'knn_access_to_subway'},
]
)
Time spent: 0s [Done 118 of 118 origins (100.00%)]
[22]:
We notice many buildings that could reach two subway stations recieved a full KNN Access score of 1, but many nuildings that could only access one station, only recieved 0.75, the weight given for being able to access one destination.
it is possible to account for impact of dystance by setting the parameter knn_plateau. This works by imposing a gravity-like penalty on destinations that are further than the specified plateau. for instance, if we set knn_plateau=200, this would lead to imposing a gravity penalty on each destination trhat is further than 200 meters from the origin. The penalty is only applied to the distance that exceeds the specified knn_plateau, not the entire distance as in the gravity metric
shown earlier. knn_plateau shoudl be between 0 and the search radius. when its set to zero, a penalty is always applied to distance. if its set to exactly the search radius, a penalty would never be applied. if the knn_plateau us setm beta needs to also be set to define the penalty applied.
[23]:
una.accessibility(
cambridge,
search_radius=300,
beta=0.004,
knn_weights=[0.75, 0.25],
knn_plateau=200,
save_knn_access_as='knn_plateau_access_to_subway'
)
cambridge.create_map(
[
{"layer": 'sidewalks', 'color': [135, 125, 125]},
{"layer": 'buildings', "color_by_attribute": "knn_plateau_access_to_subway", 'color_method':'gradient', 'text': 'knn_plateau_access_to_subway'},
]
)
Time spent: 0s [Done 118 of 118 origins (100.00%)]
[23]:
In this case, we notice that some of the origins that recieved a full 1 KNN access score earlier, recieved less than 1 if either of the two ‘suffecient’ stations is further than 200 meters away.
Saving output into files
for buildings, we’ve identified the closest_subway and the distance_to_closest_subway, and measured reach_to_subway, reach_to_daily_trains, gravity_to_subway, gravity_to_daily_trains:
[24]:
cambridge['buildings'].gdf.head(5)
[24]:
| __GUID | people | geometry | students | closest_subway | distance_to_closest_subway | reach_to_subway | reach_to_daily_trains | gravity_to_subway | gravity_to_daily_trains | knn_access_to_subway | knn_plateau_access_to_subway | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| id | ||||||||||||
| 0 | a3cf62e2-717b-43c8-a526-5a99885c120d | 14.0 | POINT (-1795.759 257.418) | NaN | 1.0 | 231.906295 | 1.0 | 50.0 | 0.395492 | 19.774591 | 0.75 | 0.660137 |
| 1 | 115b9c2b-dc80-46da-8ee3-ec9fe2a0e9cd | 25.0 | POINT (-1761.508 237.813) | NaN | 1.0 | 192.478819 | 2.0 | 70.0 | 0.781277 | 29.517112 | 1.00 | 0.927056 |
| 2 | 50865a79-e448-4d41-a4d0-7a13bc643fb3 | 31.0 | POINT (-1695.133 233.561) | 10.0 | 1.0 | 143.089733 | 2.0 | 70.0 | 0.998544 | 36.896673 | 1.00 | 0.991667 |
| 3 | 0a59c0eb-1c28-4222-b0e8-60a0bd65f46f | 50.0 | POINT (-1783.476 226.711) | NaN | 1.0 | 203.445087 | 1.0 | 50.0 | 0.443180 | 22.158976 | 0.75 | 0.739736 |
| 4 | efbad667-b385-41db-be34-94e8d3540bb9 | 3.0 | POINT (-1760.327 211.357) | NaN | 1.0 | 175.973763 | 2.0 | 70.0 | 0.795933 | 30.758300 | 1.00 | 0.917627 |
And for subway stations, we calculated reach_to_buildings, reach_to_students, gravity_to_students, reach_to_people, gravity_to_people, reach_to_closest_people, gravity_to_closest_people, and defined a new attribute daily_trains
[25]:
cambridge['subway'].gdf.head(5)
[25]:
| __GUID | geometry | reach_to_buildings | reach_to_students | gravity_to_students | reach_to_people | gravity_to_people | reach_to_closest_people | gravity_to_closest_people | daily_trains | |
|---|---|---|---|---|---|---|---|---|---|---|
| id | ||||||||||
| 0 | 172532b6-bb9b-493b-9a23-43816162b3ba | POINT (-1503.633 263.117) | 106.0 | 20.0 | 16.883378 | 2789.0 | 2361.535495 | 1265.0 | 1120.918291 | 20.0 |
| 1 | 1d26484f-051f-4ec6-a180-8c4556c33ea2 | POINT (-1635.436 137.205) | 112.0 | 10.0 | 8.666763 | 3017.0 | 2606.806614 | 1914.0 | 1716.573737 | 50.0 |
We could save these layers to a few file formats, by using the Geopandas’ ``to_file()` <https://geopandas.org/en/stable/docs/reference/api/geopandas.GeoDataFrame.to_file.html>`__ function, which can create multiple file formats. Also, you could use any of Pandas’ output functions. Notice that field names/column names in shapefiles cannot exceed 10 characters.
[26]:
# Shapefiles
cambridge['buildings'].gdf.to_file('building_accessibility.shp')
cambridge['subway'].gdf.to_file('subway_accessibility.shp')
# GeoJSON
cambridge['buildings'].gdf.to_file('building_accessibility.geojson', driver='GeoJSON')
cambridge['subway'].gdf.to_file('subway_accessibility.geojson', driver='GeoJSON')
# CSV
cambridge['buildings'].gdf.to_csv('building_accessibility.csv')
cambridge['subway'].gdf.to_csv('subway_accessibility.csv')
C:\Users\abdul\AppData\Local\Temp\ipykernel_16024\2894655930.py:2: UserWarning: Column names longer than 10 characters will be truncated when saved to ESRI Shapefile.
cambridge['buildings'].gdf.to_file('building_accessibility.shp')
C:\Users\abdul\AppData\Local\Temp\ipykernel_16024\2894655930.py:3: UserWarning: Column names longer than 10 characters will be truncated when saved to ESRI Shapefile.
cambridge['subway'].gdf.to_file('subway_accessibility.shp')