cityseer is a collection of computational tools for fine-grained street-network and land-use analysis, useful for assessing the morphological precursors to vibrant neighbourhoods.
cityseer is underpinned by network-based methods that have been developed from the ground-up for localised urban analysis at the pedestrian scale, with the intention of providing contextually specific metrics for any given streetfront location. It can be used to compute a variety of node or segment-based centrality methods, landuse accessibility and mixed-use measures, and statistical aggregations. Aggregations are computed dynamically — directly over the network while taking into account the direction of approach — and can incorporate spatial impedances and network decomposition to further accentuate spatial precision.
The use of
python facilitates interaction with popular computational tools for network manipulation (e.g.
networkX), geospatial data processing (e.g.
shapely, etc.), Open Street Map workflows with
OSMnx, and interaction with the
numpy stack of scientific packages. The underlying algorithms are are implemented in
numba JIT compiled code so that the methods can be applied to large decomposed networks. In-out convenience methods are provided for interfacing with
networkX and graph cleaning tools aid the incorporation of messier network representations such as those derived from Open Street Map.
The github repository is available at github.com/benchmark-urbanism/cityseer-api. Associated papers introducing the package and demonstrating the forms of analysis that can be done with it are available at
cityseer is a
python package that can be installed with
pip install cityseer
Code tests are run against
python 3.9, though the code base will generally be compatible with other recent versions of
A notebook of this guide can be found at google colaboratory.
cityseer revolves around networks (graphs). If you’re comfortable with
numpy and abstract data handling, then the
underlying data structures can be created and manipulated directly. However, it is generally more convenient to sketch
the graph using
NetworkX and to let
cityseer take care of initialising and converting
the graph for you.
# any networkX MultiGraph with 'x' and 'y' node attributes will do # here we'll use the cityseer mock module to generate an example networkX graph import networkx as nx from cityseer.tools import mock, graphs, plot G = mock.mock_graph() print(nx.info(G), '\n') # let's plot the network plot.plot_nX(G, labels=True, node_size=80, dpi=100)
An example graph.
tools.graphs module contains a collection of convenience functions for the preparation and conversion of
MultiGraphs, i.e. undirected graphs allowing for parallel edges. These functions are designed to work with raw
Linestring geometries that have been assigned to the edge (link)
geom attributes. The benefit to this approach is that the geometry of the network is decoupled from the topology: the topology is consequently free from distortions which would otherwise confound centrality and other metrics.
There are generally two scenarios when creating a street network graph:
In the ideal case, if you have access to a high-quality street network dataset – which keeps the topology of the network separate from the geometry of the streets – then you would construct the network based on the topology while assigning the roadway geometries to the respective edges spanning the nodes. OS Open Roads is a good example of this type of dataset. Assigning the geometries to an edge involves A) casting the geometry to a
LineString, and B) assigning this geometry to the respective edge by adding the
LineStringgeometry as a
G.add_edge(start_node, end_node, geom=a_linestring_geom).
In reality, most data-sources are not this refined and will represent roadway geometries by adding additional nodes to the network. For a variety of reasons, this is not ideal and you may want to follow the
Graph Cleaningguide; in these cases, the
graphs.nX_simple_geomsmethod can be used to generate the street geometries, after which several methods can be applied to clean and prepare the graph. For example,
nX_wgs_to_utmaids coordinate conversions;
nX_remove_dangling_nodesremoves remove roadway stubs,
nX_remove_filler_nodesstrips-out filler nodes, and
nX_consolidate_nodesassists in cleaning-up the network.
Here, we’ll walk through a high-level overview showing how to use
cityseer. You can provide your own shapely geometries if available; else, you can auto-infer simple geometries from the start to end node of each network edge, which works well for graphs where nodes have been used to inscribe roadway geometries.
G = graphs.nX_simple_geoms(G) plot.plot_nX(G, labels=True, node_size=80, plot_geoms=True, dpi=100)
A graph with inferred geometries. In this case the geometries are all exactly straight.
We have now inferred geometries for each edge, meaning that each edge now has an associated
LineString geometry. Any further manipulation of the graph using the
cityseer.graph module will retain and further manipulate these geometries in-place.
G_decomp = graphs.nX_decompose(G, 50) plot.plot_nX(G_decomp, plot_geoms=True, labels=False, dpi=100)
A decomposed graph.
# optionally cast to a dual network G_dual = graphs.nX_to_dual(G) # here we are plotting the newly decomposed graph (blue) against the original graph (red) plot.plot_nX_primal_or_dual(G, G_dual, plot_geoms=False, dpi=100)
A dual graph (blue) plotted against the primal source graph (red). In this case, the true geometry has not been plotted so that the dual graph is easily delineated from the primal graph.
networkX graph can now be transformed into a
NetworkLayer by invoking
NetworkLayerFromNX. Network layers are used for network centrality computations and also provide the backbone for subsequent landuse and statistical aggregations. They must be initialised with a set of distances specifying the maximum network-distance thresholds at which the local centrality methods will terminate.
NetworkLayer.segment_centrality methods wrap underlying numba optimised functions that compute a range of centrality methods. All selected measures and distance thresholds are computed simultaneously to reduce the amount of time required for multi-variable and multi-scalar workflows. The results of the computations will be written to the
NetworkLayer class, and can be accessed at the
NetworkLayer.metrics property. It is also possible to extract the data to a
python dictionary through use of the
NetworkLayer.metrics_to_dict method, or to simply convert the network — data and all — back into a
networkX layer with the
from cityseer.metrics import networks # create a Network layer from the networkX graph N = networks.NetworkLayerFromNX(G_decomp, distances=[200, 400, 800, 1600]) # the underlying method allows the computation of various centralities simultaneously, e.g. N.segment_centrality(measures=['segment_harmonic', 'segment_betweenness'])
DataLayer represents data points. A
DataLayer can be assigned to a
NetworkLayer, which means that each data point will be associated with the two closest network nodes — one in either direction — based on the closest adjacent street edge. This enables
cityseer to use dynamic spatial aggregation methods that more accurately describes distances from the perspective of pedestrians travelling over the network, and relative to the direction of approach.
from cityseer.metrics import layers # a mock data dictionary representing the 'x', 'y' attributes for data points data_dict = mock.mock_data_dict(G_decomp, random_seed=25) print(data_dict, data_dict, 'etc.') # generate a data layer D = layers.DataLayerFromDict(data_dict) # assign to the prior Network Layer # max_dist represents the farthest to search for adjacent street edges D.assign_to_network(N, max_dist=400) # let's plot the assignments plot.plot_assignment(N, D, dpi=100)
Data points assigned to a Network Layer.
Data assignment becomes more precise on a decomposed Network Layer.
Once the data has been assigned, the
DataLayer.compute_landuses method is used for the calculation of mixed-use and land-use accessibility measures whereas
DataLayer.compute_stats can likewise be used for statistical measures. As with the centrality methods, the measures are all computed simultaneously (and for all distances); however, simpler stand-alone methods are also available, including
Landuse labels can be used to generate mixed-use and land-use accessibility measures. Let’s create mock landuse labels for the points in our data dictionary and compute mixed-uses and land-use accessibilities:
landuse_labels = mock.mock_categorical_data(len(data_dict), random_seed=25) print(landuse_labels) # example easy-wrapper method for computing mixed-uses D.hill_branch_wt_diversity(landuse_labels, qs=[0, 1, 2]) # example easy-wrapper method for computing accessibilities # the keys correspond to keys present in the landuse data # for which accessibilities will be computed D.compute_accessibilities(landuse_labels, accessibility_keys=['a', 'c']) # or compute multiple measures at once, e.g.: D.compute_landuses(landuse_labels, mixed_use_keys=['hill', 'hill_branch_wt', 'shannon'], accessibility_keys=['a', 'c'], qs=[0, 1, 2])
We can do the same thing with numerical data. Let’s generate some mock numerical data:
mock_valuations_data = mock.mock_numerical_data(len(data_dict), random_seed=25) print(mock_valuations_data) # compute max, min, mean, mean-weighted, variance, and variance-weighted D.compute_stats('valuations', mock_valuations_data)
The data is aggregated and computed over the street network relative to the
NetworkLayer (i.e. street) nodes. The mixed-use, accessibility, and statistical aggregations can therefore be compared directly to centrality computations from the same locations, and can be correlated or otherwise compared. The outputs of the calculations are written to the corresponding node indices in the same
NetworkLayer.metrics dictionary used for centrality methods, and will be categorised by the respective keys and parameters.
# access the data arrays at the respective keys, e.g. distance_idx = 800 # any of the initialised distances q_idx = 0 # q index: any of the invoked q parameters # centrality print('centrality keys:', list(N.metrics['centrality'].keys())) print('distance keys:', list(N.metrics['centrality']['segment_harmonic'].keys())) print(N.metrics['centrality']['segment_harmonic'][distance_idx][:4]) # mixed-uses print('mixed-use keys:', list(N.metrics['mixed_uses'].keys())) # here we are indexing in to the specified q_idx, distance_idx print(N.metrics['mixed_uses']['hill_branch_wt'][q_idx][distance_idx][:4]) # statistical keys can be retrieved the same way: print('stats keys:', list(N.metrics['stats'].keys())) print('valuations keys:', list(N.metrics['stats']['valuations'].keys())) print('valuations weighted by 1600m decay:', N.metrics['stats']['valuations']['mean_weighted'][:4]) # the data can also be convert back to a NetworkX graph G_metrics = N.to_networkX() print(nx.info(G_metrics)) # the data arrays are unpacked accordingly print(G_metrics.nodes['metrics']['centrality']['segment_betweenness']) # and can also be extracted to a dictionary: G_dict = N.metrics_to_dict() print(G_dict['centrality']['segment_betweenness'])
The data can then be passed to data analysis or plotting methods. For example, the
tools.plot module can be used to plot the segmentised harmonic closeness centrality and mixed uses for the above mock data:
# plot centrality from matplotlib import colors segment_harmonic_vals =  mixed_uses_vals =  for node, data in G_metrics.nodes(data=True): segment_harmonic_vals.append(data['metrics']['centrality']['segment_harmonic']) mixed_uses_vals.append(data['metrics']['mixed_uses']['hill_branch_wt']) # custom colourmap cmap = colors.LinearSegmentedColormap.from_list('cityseer', ['#64c1ff', '#d32f2f']) # normalise the values segment_harmonic_vals = colors.Normalize()(segment_harmonic_vals) # cast against the colour map segment_harmonic_cols = cmap(segment_harmonic_vals) # plot segment_harmonic plot.plot_nX(G_metrics, labels=False, node_colour=segment_harmonic_cols, dpi=100)
800m segmentised harmonic centrality.
# plot distance-weighted hill mixed uses mixed_uses_vals = colors.Normalize()(mixed_uses_vals) mixed_uses_cols = cmap(mixed_uses_vals) plot.plot_assignment(N, D, node_colour=mixed_uses_cols, data_labels=landuse_labels, dpi=100)
400m distance-weighted mixed-uses.