Accessibility-based characters

Linking points of interest and similar point-based data to enclosed tessellation using network accessibility approach.

For each enclosed tessellation cell, we want to know how many POIs are within 15 minutes walking distance (translated to 1200m). Furhtemore, we want to know how far is the closes one.

We use pandana package to generate accessibility characters. Pandana uses efficient contraction hierarchies algorithm to measure the shortes path in a network. Therefore, we link each enclosed cell to a node of a network (the actual linking has been already done in previous steps). Then we link each POI to a node of network and compute distances between nodes. Resulting values are then transferred from nodes back to tessellation. Note that all tessellation cells attached to a single node will share the same value.

import geopandas as gpd
import pandana
import pandas as pd
from shapely.geometry import box
from tqdm import tqdm
import pygeos

Create network

Load both parts of street network, i.e. nodes and edges (output of momepy.nx_to_gdf).

edges = gpd.read_parquet('../../urbangrammar_samba/spatial_signatures/morphometrics/edges/edges_0.pq')
nodes = gpd.read_parquet('../../urbangrammar_samba/spatial_signatures/morphometrics/nodes/nodes_0.pq')

Create pandana.Network object. We already have all necessary in our two GeoDataFrames.

nodes = nodes.set_index('nodeID')
%%time
network = pandana.Network(nodes.geometry.x, nodes.geometry.y, 
                          edges['node_start'], edges['node_end'], edges[['mm_len']])
CPU times: user 4.84 s, sys: 137 ms, total: 4.98 s
Wall time: 407 ms

Find nearest POIs

Load points of interest, clip the to the extent of the chunk, link to network and find nearest to each network node.

pois = pd.read_csv('../../urbangrammar_samba/functional_data/pois/GEOLYTIX - RetailPoints/geolytix_retailpoints_v17_202008.csv')
pois = gpd.GeoDataFrame(pois, geometry=gpd.points_from_xy(pois.bng_e, pois.bng_n), crs=27700)
pois = gpd.clip(pois, box(*nodes.total_bounds))
%%time
network.set_pois(category = 'supermarkets',
                 maxdist = 1200,
                 maxitems=100,
                 x_col = pois.bng_e, 
                 y_col = pois.bng_n)
CPU times: user 52.8 ms, sys: 8.15 ms, total: 61 ms
Wall time: 53.3 ms
%%time
results = network.nearest_pois(distance = 1200,
                               category = 'supermarkets',
                               num_pois = 100,
                               include_poi_ids = False)
CPU times: user 3.01 s, sys: 120 ms, total: 3.13 s
Wall time: 2.68 s

We are interested in a distance to the nearest (if wihtin 15 minutes threshold) and number of POIs within the threshold.

counts = results.replace(1200, pd.NA).count(axis=1)
nodes['food'] = counts
nodes.plot('food', figsize=(12, 12), markersize=(.1), scheme='quantiles')
<AxesSubplot:>
../_images/accessibility_15_1.png
min_distance = results.replace(1200, pd.NA).min(axis=1)
nodes['food_distance'] = min_distance
nodes.plot('food_distance', figsize=(12, 12), markersize=(.1), scheme='quantiles', missing_kwds={'color': 'grey', 'markersize': .1})
<AxesSubplot:>
../_images/accessibility_18_1.png

Attach distant nodes

Some nodes are further away than 15 minutes from a POI, but we are intereseted how far they are. Therefore, we run pandana again, for a single item but a large distance ensuring each node gets attached.

%%time
network.set_pois(category = 'supermarkets',
                 maxdist = 50000,
                 maxitems=1,
                 x_col = pois.bng_e, 
                 y_col = pois.bng_n)
CPU times: user 50.4 ms, sys: 12.9 ms, total: 63.3 ms
Wall time: 56.2 ms
%%time
results = network.nearest_pois(distance = 50000,
                               category = 'supermarkets',
                               num_pois = 1,
                               include_poi_ids = False)
CPU times: user 2.28 s, sys: 33 ms, total: 2.31 s
Wall time: 151 ms
nodes['nearest'] = results[1].replace(50000, pd.NA)
nodes.plot('nearest', figsize=(12, 12), markersize=(.01), scheme='quantiles', missing_kwds={'color': 'red', 'markersize': 1})
<AxesSubplot:>
../_images/accessibility_23_1.svg

Although there are still some nodes which are not connected, that is likely due to disconections of the network.

Measure accessiblity characters

Now, we measure accessibility to:

  • supermarkets (using Geolytix data)

  • listed buildings

  • FHRS points

  • cultural venues

Cultural venues are often both points and polygons, therefore we remove duplicates.

Pandana is multi-threaded, therefore we do not need to use dask to parallelise the operation on a single machine. It is also a memory-heavy operation, so we would not fit with multiple processes anyway.

supermarkets = pd.read_csv('../../urbangrammar_samba/functional_data/pois/GEOLYTIX - RetailPoints/geolytix_retailpoints_v17_202008.csv')
listed = gpd.read_parquet('../../urbangrammar_samba/functional_data/pois/listed_buildings/listed_buildings_gb.pq')
fhrs = pd.read_csv('../../urbangrammar_samba/functional_data/fhrs/Data/fhrs_location_20200528.csv')
culture = gpd.read_parquet('../../urbangrammar_samba/functional_data/pois/culture_gb.pq')

supermarkets = gpd.GeoDataFrame(supermarkets, geometry=gpd.points_from_xy(supermarkets.bng_e, supermarkets.bng_n), crs=27700)
fhrs = gpd.GeoDataFrame(fhrs, geometry=gpd.points_from_xy(fhrs.bng_east, fhrs.bng_north), crs=27700)

culture = culture.reset_index(drop=True)
inp, res = pygeos.STRtree(culture.geometry.values.data).query_bulk(culture.geometry.values.data, predicate='contains_properly')
culture = culture.drop(inp)
culture.geometry = culture.centroid
for c in tqdm(range(103), total=103):
    nodes = gpd.read_parquet(f'../../urbangrammar_samba/spatial_signatures/morphometrics/nodes/nodes_{c}.pq')
    edges = gpd.read_parquet(f'../../urbangrammar_samba/spatial_signatures/morphometrics/edges/edges_{c}.pq')
    nodes = nodes.set_index('nodeID')
    network = pandana.Network(nodes.geometry.x, nodes.geometry.y, 
                          edges['node_start'], edges['node_end'], edges[['mm_len']])
    network.precompute(1200)

    supermarkets_c = gpd.clip(supermarkets, box(*nodes.total_bounds))
    network.set_pois(category = 'supermarkets',
                     maxdist = 1200,
                     maxitems=1000,
                     x_col = supermarkets_c.bng_e, 
                     y_col = supermarkets_c.bng_n)

    res = network.nearest_pois(distance = 1200,
                               category = 'supermarkets',
                               num_pois = 1000 if len(supermarkets_c) > 1000 else len(supermarkets_c),
                               include_poi_ids = False)
    
    res = res.replace(1200, pd.NA)
    nodes['supermarkets_nearest'] = res.min(axis=1)
    nodes['supermarkets_counts'] = res.count(axis=1)

    listed_c = gpd.clip(listed, box(*nodes.total_bounds)).explode()
    network.set_pois(category = 'listed',
                     maxdist = 1200,
                     maxitems=10000,
                     x_col = listed_c.geometry.x, 
                     y_col = listed_c.geometry.y)

    res = network.nearest_pois(distance = 1200,
                               category = 'listed',
                               num_pois = 10000 if len(listed_c) > 10000 else len(listed_c),
                               include_poi_ids = False)
    
    res = res.replace(1200, pd.NA)
    nodes['listed_nearest'] = res.min(axis=1)
    nodes['listed_counts'] = res.count(axis=1)


    fhrs_c = gpd.clip(fhrs, box(*nodes.total_bounds))
    network.set_pois(category = 'fhrs',
                     maxdist = 1200,
                     maxitems=10000,
                     x_col = fhrs_c.bng_east, 
                     y_col = fhrs_c.bng_north)

    res = network.nearest_pois(distance = 1200,
                               category = 'fhrs',
                               num_pois = 10000 if len(fhrs_c) > 10000 else len(fhrs_c),
                               include_poi_ids = False)

    res = res.replace(1200, pd.NA)
    nodes['fhrs_nearest'] = res.min(axis=1)
    nodes['fhrs_counts'] = res.count(axis=1)

    culture_c = gpd.clip(culture, box(*nodes.total_bounds))
    network.set_pois(category = 'culture',
                     maxdist = 1200,
                     maxitems=500,
                     x_col = culture_c.geometry.x, 
                     y_col = culture_c.geometry.y)

    res = network.nearest_pois(distance = 1200,
                               category = 'culture',
                               num_pois = 500 if len(culture_c) > 500 else len(culture_c),
                               include_poi_ids = False)

    res = res.replace(1200, pd.NA)
    nodes['culture_nearest'] = res.min(axis=1)
    nodes['culture_counts'] = res.count(axis=1)

    cells = gpd.read_parquet(f'../../urbangrammar_samba/spatial_signatures/morphometrics/cells/cells_{c}.pq')
    cells = cells.merge(nodes, on='nodeID', how='left')
    cells[['hindex', 'supermarkets_nearest', 'supermarkets_counts',
           'listed_nearest', 'listed_counts', 'fhrs_nearest',
           'fhrs_counts', 'culture_nearest', 'culture_counts'
          ]
         ].to_parquet(f"../../urbangrammar_samba/spatial_signatures/functional/accessibility/access_{c}.pq")

Attach distant nodes

for c in tqdm(range(103), total=103):
    nodes = gpd.read_parquet(f'../../urbangrammar_samba/spatial_signatures/morphometrics/nodes/nodes_{c}.pq')
    edges = gpd.read_parquet(f'../../urbangrammar_samba/spatial_signatures/morphometrics/edges/edges_{c}.pq')
    nodes = nodes.set_index('nodeID')
    network = pandana.Network(nodes.geometry.x, nodes.geometry.y, 
                          edges['node_start'], edges['node_end'], edges[['mm_len']])
    network.precompute(50000)

    supermarkets_c = gpd.clip(supermarkets, box(*nodes.total_bounds))
    network.set_pois(category = 'supermarkets',
                     maxdist = 50000,
                     maxitems=1,
                     x_col = supermarkets_c.bng_e, 
                     y_col = supermarkets_c.bng_n)

    res = network.nearest_pois(distance = 50000,
                               category = 'supermarkets',
                               num_pois = 1,
                               include_poi_ids = False)
    
    nodes['supermarkets_nearest'] = res[1].replace(50000, pd.NA)

    listed_c = gpd.clip(listed, box(*nodes.total_bounds)).explode()
    network.set_pois(category = 'listed',
                     maxdist = 50000,
                     maxitems=1,
                     x_col = listed_c.geometry.x, 
                     y_col = listed_c.geometry.y)

    res = network.nearest_pois(distance = 50000,
                               category = 'listed',
                               num_pois = 1,
                               include_poi_ids = False)
    
    nodes['listed_nearest'] = res[1].replace(50000, pd.NA)


    fhrs_c = gpd.clip(fhrs, box(*nodes.total_bounds))
    network.set_pois(category = 'fhrs',
                     maxdist = 50000,
                     maxitems=1,
                     x_col = fhrs_c.bng_east, 
                     y_col = fhrs_c.bng_north)

    res = network.nearest_pois(distance = 50000,
                               category = 'fhrs',
                               num_pois = 1,
                               include_poi_ids = False)

    nodes['fhrs_nearest'] = res[1].replace(50000, pd.NA)

    culture_c = gpd.clip(culture, box(*nodes.total_bounds))
    network.set_pois(category = 'culture',
                     maxdist = 50000,
                     maxitems=1,
                     x_col = culture_c.geometry.x, 
                     y_col = culture_c.geometry.y)

    res = network.nearest_pois(distance = 50000,
                               category = 'culture',
                               num_pois = 1,
                               include_poi_ids = False)

    nodes['culture_nearest'] = res[1].replace(50000, pd.NA)

    cells = gpd.read_parquet(f'../../urbangrammar_samba/spatial_signatures/morphometrics/cells/cells_{c}.pq')
    cells = cells.merge(nodes, on='nodeID', how='left')
    
    acc = pd.read_parquet(f"../../urbangrammar_samba/spatial_signatures/functional/accessibility/access_{c}.pq")
    acc['supermarkets_nearest'] = cells['supermarkets_nearest']
    acc['listed_nearest'] = cells['listed_nearest']
    acc['fhrs_nearest'] = cells['fhrs_nearest']
    acc['culture_nearest'] = cells['culture_nearest']
    acc.to_parquet(f"../../urbangrammar_samba/spatial_signatures/functional/accessibility/access_{c}.pq")