Introduction to uci: urban centrality index

uci is an R package to measure the centrality of cities and regions. The package implements the Urban Centrality Index (UCI) originally proposed by Pereira et al. (2013). UCI measures the extent to which the spatial organization of a city varies from monocentric to polycentric in a continuous scale, rather than considering a binary classification (either monocentric or polycentric). UCI values range from 0 to 1. Values closer to 0 indicate more polycentric patterns and values closer to 1 indicate a more monocentric urban form.

Installation

# from CRAN
install.packages('uci')

# development version from GitHub
remotes::install_github("ipea/uci")

Demonstration on sample data

First, let’s load a few libraries we’ll be using in this vignette.

library(uci)
library(sf)
library(ggplot2)

Data input

The uci package comes with a sample data for demonstration and test purposes. The data is a small sample of the spatial distribution of the population, jobs and schools around the city center of Belo Horizonte, Brazil. This sample data set is a good illustration of the type of data input required by uci.

Here is how the sample data looks like:

data_dir <- system.file("extdata", package = "uci")

grid <- readRDS(file.path(data_dir, "grid_bho.rds"))
head(grid)
#> Simple feature collection with 6 features and 4 fields
#> Geometry type: POLYGON
#> Dimension:     XY
#> Bounding box:  xmin: -43.96438 ymin: -19.97414 xmax: -43.93284 ymax: -19.96717
#> Geodetic CRS:  WGS 84
#>                id population jobs schools                       geometry
#> 1 89a881a5a2bffff        439  180       0 POLYGON ((-43.9431 -19.9741...
#> 2 89a881a5a2fffff        266  134       0 POLYGON ((-43.94612 -19.972...
#> 3 89a881a5a67ffff       1069  143       0 POLYGON ((-43.94001 -19.972...
#> 4 89a881a5a6bffff        245   61       0 POLYGON ((-43.9339 -19.9728...
#> 5 89a881a5a6fffff        298   11       0 POLYGON ((-43.93691 -19.971...
#> 6 89a881a5b03ffff        555 1071       0 POLYGON ((-43.96136 -19.970...

The data is an object of class "sf" "data.frame" with spatial polygons covering our study area and a few columns indicating the number of activities (e.g. jobs, schools, population) in each polygon. Our particular sample data is based on a spatial hexagonal grid (H3 index). While there are advantages of using regular spatial grids to calculate spatial statistics, uci also works with non-regular geometries, such as census tracts, enumeration areas or municipalities.

We can visualize the spatial distribution of jobs using ggplot2:


ggplot(data = grid) +
  geom_sf(aes(fill = jobs), color = NA) +
  scale_fill_viridis_c() +
  theme_void()

Calculating UCI

In the example below, we calculate how mono/polycentric our study area is considering its spatial distribution of jobs considering Euclidian distances between polygons.

df <- uci(
  sf_object = grid,
  var_name = 'jobs', 
  dist_type = 'euclidean'
  )

df
#>         UCI location_coef spatial_separation spatial_separation_max
#> 1 0.2612105     0.5278007           3880.114               7681.927

By default, the uci() function uses Euclidean distances between polygons. However, Euclidean distances can lead to misleading results in the case of areas with a concave shape (like a bay). In these cases, it is strongly recommended to use dist_type = 'spatial_link'. These spatial link distances consider Euclidean distances along the links of spatial neighbour links. This is a bit more costly computationally speaking, but it does return more realistic results.

Mind you, though, that when using "spatial_link" distances, there should be no disconnected island of polygon (or group of polygons). In some cases, users might need to manually edit their spatial data to include a polygon representing a road or bridge connecting disconnected parts of the area.

df <- uci(
  sf_object = grid,
  var_name = 'jobs', 
  dist_type = 'spatial_link'
  )

df
#>         UCI location_coef spatial_separation spatial_separation_max
#> 1 0.2609465     0.5278007           4281.819               8468.847

Where there are no bridges or roads connecting isolated polygons, we can remove them by passing the arugment remove_islands = TRUE. Let’s see this in another example data set using the city of Rio de Janeiro, Brazil.

The grid_rio.rds data set covers the municipality of Rio, with a concave outline around the Guanabara and Sepetiba bays and several islands, including single isolated cells. The data comes from the Access to Opportunities Project (AOP, Ipea, 2019), aggregated to H3 resolution 8.

grid_rio <- readRDS(file.path(data_dir, "grid_rio.rds"))
ggplot(data = grid_rio) +
  geom_sf(aes(fill = jobs), color = NA) +
  scale_fill_viridis_c() +
  theme_void()

As you can see, the case of Rio include a few islands disconnected from mainland. Because of these islands, we can only consider the full grid in the calculation of UCI if we use dist_type = 'euclidean'.


df <- uci(
  sf_object = grid_rio,
  var_name = 'jobs',
  dist_type = 'euclidean'
  )
#> Warning: The study area has a concave shape or a large gap inside it (e.g. a bay or a
#> lake). Euclidean distances between centroids can cross areas outside it and
#> bias UCI.
#> ℹ Consider `dist_type = "spatial_link"`.

df
#>         UCI location_coef spatial_separation spatial_separation_max
#> 1 0.4201327      0.693971           17632.32               44684.47

The warning is expected: Rio’s coastline is concave, so Euclidean distances between centroids can cross the bays.

Alternatively, you use dist_type = 'spatial_link' and pass remove_islands = TRUE. In this case, uci() drops the islands calculates UCI only for the largest group of neighbouring polygons. For large areas, users can speed up the process of calculating ‘spatial_link’ distances by passing parallel = FALSE.

df <- uci(
  sf_object = grid_rio,
  var_name = 'jobs',
  dist_type = 'spatial_link',
  remove_islands = TRUE,
  parallel = FALSE
  )
#> Removed 44 polygons (255 jobs) outside the largest contiguous area of
#> `sf_object`.

df
#>         UCI location_coef spatial_separation spatial_separation_max
#> 1 0.4045099     0.6894746           19331.93               46773.76

To see which polygons are dropped before calculating UCI, use the function remove_islands(). It returns the sf object with only the largest group of neighbouring polygons, which you can map and then pass to uci().

Formal definition of UCI

The Urban Centrality Index (UCI) is given by the product of the Location Coefficient (LC) and the Proximity Index (PI). With this formulation, UCI calculates urban centrality simultaneously accounting for both the concentration of activities and the proximity between activities.

\[ \begin{aligned} UCI = LC * PI \end{aligned} \] The Location Coefficient (LC) was introduced by Florence (1948) to measure the unequal distribution factor of jobs within an urban area. It works as a nonspatial inequality measure (similarly to Gini, for example). It reflects, for example, the extent to which the number of activities are concentrated in few polygons. The range of the LC is 0 to (1 - 1/n). If LC equals zero, then economic activity is evenly distributed, while values close to (1 - 1/n) indicate that employment is concentrated in a few areas.

\[ \begin{aligned} LC = \frac{1}{2} \sum_{1}^{n}∣S_{i} − \frac{1}{n}​|\\ S_{i} = \frac{E_{i}}{E} \end{aligned} \]

where Si is the share of employment in area i (Ei) relative to the total employment (E) of the city; E is the total number of jobs in a city; and n = number of areas.

Meanwhile, the Proximity Index (PI) is a transformation that Pereira et al. (2013) proposed for the Venables index, originally developed by Midelfart-Knarvik et al. (2002) to examine changes in the spatial distribution of economic activity. The Venables index (aka spatial separation index) is defined as:

\[ \begin{aligned} V = S' * D * S \end{aligned} \] where S is a column vector of Si; and D is a distance matrix whose entry dij is the distance between the centroids of areas i and j.

When all employment activity is concentrated in just one spatial unit, the minimum value of V is reached; that is, zero (no matter where this spatial unit is located). However, the index has no maximum value and therefore cannot be compared across different spatial settings. To overcome this limitation, it is necessary to calculate the maximum attainable value of V.

The Proximity Index (PI) solves the normalization issue with V and changes its interpretation to suit our needs. The interpretation of PI is the opposite of V, with its theoretical range being (0, 1). Values of PI closer to 1 mean that employment is clustered in one single center. If PI is 0, employment is as spatially separated as possible. In other words, activities are distributed in a way that maximizes the distances between them.

\[ \begin{aligned} P = 1 - \frac{V}{V_{max}} \end{aligned} \]

The normalization by Vmax is what makes the comparison of urban areas of different shapes and sizes possible. However, the estimation of Vmax is not trivial, because it has no closed-form solution. In a very simple square grid, Vmax is obtained when each corner has one-fourth of the total employment. In a region forming a perfect circle, the maximum value of V occurs when all employment is evenly distributed along the external edge.

Finding Vmax

By default (max_method = "exact"), uci() searches for the distribution of activities among the polygons on the border of the study area that maximises V. With Euclidean distances between centroids this problem is concave, so the search (a simple multiplicative update of the weights, stopped when the optimality conditions hold) reaches the global maximum. The result is then refined with the self distance of each polygon included, so that Vmax is a stationary point of the same quantity as V, and checked against the polygons outside the border. With "spatial_link" distances the same procedure gives a stationary point.

df_exact <- uci(
  sf_object = grid,
  var_name = 'jobs',
  showProgress = FALSE
)

head(df_exact)
#>         UCI location_coef spatial_separation spatial_separation_max
#> 1 0.2612105     0.5278007           3880.114               7681.927

With dist_type = "spatial_link", users can set parallel = TRUE to compute shortest-path distances using several threads.

Two other approaches are kept so that results of previous versions can be reproduced. In the original paper, Pereira et al. (2013) proposed a simple heuristic approach to calculate Vmax (the “opposite of maximum proximity”) as a homogeneous distribution of values along the edge of the study area (max_method = "heuristic"). The bootstrap approach (max_method = "bootstrap") simulates 20000 random distributions of activities along the border: it simulates that all jobs are concentrated in up to 2, 3, 4, 5 … 51 polygons along the border, and for each number of selected polygons it shuffles 400 random positions of those polygons along the border, keeping the largest V found. Both are lower bounds of Vmax, so they overestimate V / Vmax and underestimate UCI, by an amount that depends on the shape of the study area.

df_heuristic <- uci(
  sf_object = grid,
  var_name = 'jobs',
  max_method = "heuristic",
  showProgress = FALSE
)

head(df_heuristic)
#>         UCI location_coef spatial_separation spatial_separation_max
#> 1 0.2538635     0.5278007           3880.114               7475.899

References

Florence, Philip Sargant. 1948. Investment, Location and Size of Plant: A Realistic Enquiry into the Structure of British and American Industries. Vol. 8. CUP Archive.
Midelfart-Knarvik, Karen Helen, Henry G. Overman, Stephen Redding, and Anthony J. Venables. 2002. “Integration and Industrial Specialisation in the European Union.” Revue Économique 53 (3): 469–81. https://doi.org/10.2307/3502978.
Pereira, Rafael H. M., Vanessa Nadalin, Leonardo Monasterio, and Pedro H. M. Albuquerque. 2013. “Urban Centrality: A Simple Index.” Geographical Analysis 45 (1): 77–89. https://doi.org/10.1111/gean.12002.