Travel-time matrices#
What is a travel time matrix?
A travel time matrix is a table that shows the travel time between all pairs of a set of locations in an urban area. The locations represent typical origins and destinations, such as everyday services and residential homes, or are a complete set of locations covering the entire area wall-to-wall, such as census polygons or a regular grid.
A travel time matrix is a key piece of information in transportation research and planning. It allows us to study how easily people can reach different destinations, to evaluate the impacts of transport and land use policies, to analyse accessibility patterns and to examine how accessibility is influenced by factors such as the quality of public transport systems, street networks, and land use patterns. Travel time matrices can also be used to identify in which areas and for which groups of people a city works best and worst. Altogether, this makes travel time matrices a critical information to help cities become more equitable and more sustainable, and to foster a good quality of life for their residents.
Successful recent research that either used or produced travel time matrices include the work of the Digital Geography Lab at the University of Helsinki (e.g., Tenkanen and Toivonen (2020), Salonen and Toivonen (2013), or Järv et al. (2018)), the Mobility Network at the University of Toronto (e.g., Farber and Fu (2017), Farber et al. (2014)), and the Access to Opportunities Project (AOP) at the Institute for Applied Economic Research - IPEA (e.g., Pereira et al. (2021), Braga et al. (2023), Herszenhut et al. (2022)).
Load a transport network#
As briefly visited in Quickstart and dicussed in detail in Data Requirements, fundamentally, two types of input data are required for computing a travel time matrix:
a transport network, and
a set of origins and destinations
In the example below, we first create a
TransportNetwork
. To do so, we load an
OpenStreetMap extract of the São Paulo city centre as well as a public transport
schedule in GTFS format covering the same area:
import r5py
transport_network = r5py.TransportNetwork(
DATA_DIRECTORY / "São Paulo" / "spo_osm.pbf",
[
DATA_DIRECTORY / "São Paulo" / "spo_gtfs.zip",
]
)
Studies that compare accessibility between different neighbourhoods tend to use a regular grid of points that covers the study area as origins or destinations. Recently, hexagonal grids, such as Uber’s H3 indexing system have gained popularity, as they assure equidistant neighbourhood relationships (all neighbouring grid cells’ centroids are at the same distance; in a grid of squares, the diagonal neighbours are roughly 41% further than the horizontal and vertical ones).
We prepared such a hexagonal grid for São Paulo, and added the counts of
population
, jobs
, and schools
within each cell as separate columns.
The id
column refers to the H3 address of the grid cells.
import geopandas
hexagon_grid = geopandas.read_file(DATA_DIRECTORY / "São Paulo" / "spo_hexgrid_EPSG32723.gpkg.zip")
hexagon_grid
id | population | jobs | schools | geometry | |
---|---|---|---|---|---|
0 | 89a8100c603ffff | 1146 | 1155 | 0 | POLYGON ((336000.312 7392162.721, 336107.079 7... |
1 | 89a8100c617ffff | 700 | 463 | 1 | POLYGON ((335685.425 7392005.022, 335792.187 7... |
2 | 89a8100c60fffff | 377 | 257 | 0 | POLYGON ((336005.719 7392502.350, 336112.488 7... |
3 | 89a8100c607ffff | 743 | 687 | 0 | POLYGON ((335690.825 7392344.644, 335797.590 7... |
4 | 89a8100c6abffff | 601 | 29 | 0 | POLYGON ((335375.940 7392186.943, 335482.700 7... |
... | ... | ... | ... | ... | ... |
318 | 89a8100cea7ffff | 34 | 2483 | 0 | POLYGON ((330455.500 7397136.529, 330562.237 7... |
319 | 89a8100ce23ffff | 382 | 395 | 1 | POLYGON ((331090.629 7397791.927, 331197.377 7... |
320 | 89a8100ce37ffff | 10 | 202 | 0 | POLYGON ((330775.717 7397634.124, 330882.461 7... |
321 | 89a8100c8dbffff | 19 | 438 | 0 | POLYGON ((330460.813 7397476.324, 330567.552 7... |
322 | 89a8100c8cbffff | 12 | 1375 | 0 | POLYGON ((330466.126 7397816.132, 330572.867 7... |
323 rows × 5 columns
We can use explore()
to plot the
hexagonal grid in a map:
hexagon_grid.explore()
R5py expects origins and destinations to be point geometries. For grid cells,
the geometric center point (‘centroid’) is a good approximisation. One can use
geopandas.GeoDataSeries.centroid
to quickly derive a centroid (point)
geometry from a polygon. We will create one data frame for origins, and one for
destinations:
origins = hexagon_grid.copy()
origins["geometry"] = origins.geometry.centroid
destinations = hexagon_grid.copy()
destinations["geometry"] = destinations.geometry.centroid
Compute a travel time matrix#
With this, we have all input data sets needed for computing a travel time matrix: a transport network, origins, and destinations. We still need to decide which modes of transport should be used, and the departure time in our analysis.
The modes of transport can be passed as a list of different
r5py.TransportMode
s (or their str
equivalent). Meanwhile, the
departure must be a datetime.datetime
. If you search for public
transport routes, double-check that the departure date and time is covered by
the input GTFS data set.
import datetime
travel_time_matrix = r5py.TravelTimeMatrixComputer(
transport_network,
origins=origins,
destinations=destinations,
transport_modes=[r5py.TransportMode.TRANSIT],
departure=datetime.datetime(2019, 5, 13, 14, 0, 0),
).compute_travel_times()
The output of
compute_travel_times()
is a table in which each row describes the travel time (travel_time
) from an
origin (from_id
), to a destination (to_id
).
travel_time_matrix
from_id | to_id | travel_time | |
---|---|---|---|
0 | 89a8100c603ffff | 89a8100c603ffff | 0.0 |
1 | 89a8100c603ffff | 89a8100c617ffff | 13.0 |
2 | 89a8100c603ffff | 89a8100c60fffff | 6.0 |
3 | 89a8100c603ffff | 89a8100c607ffff | 11.0 |
4 | 89a8100c603ffff | 89a8100c6abffff | 20.0 |
... | ... | ... | ... |
104324 | 89a8100c8cbffff | 89a8100cea7ffff | 15.0 |
104325 | 89a8100c8cbffff | 89a8100ce23ffff | 15.0 |
104326 | 89a8100c8cbffff | 89a8100ce37ffff | 9.0 |
104327 | 89a8100c8cbffff | 89a8100c8dbffff | 6.0 |
104328 | 89a8100c8cbffff | 89a8100c8cbffff | 0.0 |
104329 rows × 3 columns
As TravelTimeMatrixComputer
creates an
all-to-all matrix in long format. In other words, the results contain one row
for every combination of origins and destinations. Since we have
323 origins and 323 destinations,
the output travel time matrix is 104329 rows long.
Alternatively, and possibly more intuitively, we can display the travel time
matrix table as a matrix in wide format, using pandas.DataFrame.pivot()
:
travel_time_matrix.pivot(index="from_id", columns="to_id", values="travel_time")
to_id | 89a8100c003ffff | 89a8100c007ffff | 89a8100c00bffff | 89a8100c00fffff | 89a8100c013ffff | 89a8100c017ffff | 89a8100c01bffff | 89a8100c023ffff | 89a8100c027ffff | 89a8100c02bffff | ... | 89a8100ddd3ffff | 89a8100ddd7ffff | 89a8100dddbffff | 89a8100ea43ffff | 89a8100ea4bffff | 89a8100ea4fffff | 89a8100ea53ffff | 89a8100ea5bffff | 89a8100eacbffff | 89a8100eadbffff |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
from_id | |||||||||||||||||||||
89a8100c003ffff | 0.0 | 9.0 | 6.0 | 9.0 | 9.0 | 6.0 | 10.0 | 22.0 | 30.0 | 24.0 | ... | 44.0 | 41.0 | 53.0 | 40.0 | 42.0 | 46.0 | 41.0 | 37.0 | 45.0 | 53.0 |
89a8100c007ffff | 9.0 | 0.0 | 11.0 | 7.0 | 13.0 | 7.0 | 17.0 | 13.0 | 22.0 | 15.0 | ... | 36.0 | 32.0 | 45.0 | 32.0 | 35.0 | 37.0 | 36.0 | 31.0 | 40.0 | 47.0 |
89a8100c00bffff | 6.0 | 11.0 | 0.0 | 9.0 | 15.0 | 12.0 | 11.0 | 24.0 | 31.0 | 23.0 | ... | 46.0 | 43.0 | 55.0 | 42.0 | 45.0 | 48.0 | 47.0 | 42.0 | 51.0 | 58.0 |
89a8100c00fffff | 9.0 | 7.0 | 9.0 | 0.0 | 16.0 | 10.0 | 17.0 | 17.0 | 25.0 | 17.0 | ... | 39.0 | 35.0 | 48.0 | 35.0 | 38.0 | 41.0 | 39.0 | 34.0 | 43.0 | 51.0 |
89a8100c013ffff | 9.0 | 13.0 | 15.0 | 16.0 | 0.0 | 10.0 | 7.0 | 26.0 | 35.0 | 28.0 | ... | 49.0 | 45.0 | 57.0 | 44.0 | 47.0 | 50.0 | 46.0 | 41.0 | 50.0 | 57.0 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
89a8100ea4fffff | 45.0 | 37.0 | 48.0 | 40.0 | 50.0 | 42.0 | 53.0 | 30.0 | 31.0 | 28.0 | ... | 37.0 | 33.0 | 46.0 | 10.0 | 11.0 | 0.0 | 18.0 | 16.0 | 26.0 | 34.0 |
89a8100ea53ffff | 41.0 | 36.0 | 46.0 | 39.0 | 46.0 | 38.0 | 49.0 | 29.0 | 33.0 | 30.0 | ... | 40.0 | 37.0 | 49.0 | 8.0 | 11.0 | 18.0 | 0.0 | 8.0 | 8.0 | 15.0 |
89a8100ea5bffff | 36.0 | 31.0 | 42.0 | 34.0 | 41.0 | 33.0 | 44.0 | 24.0 | 31.0 | 26.0 | ... | 38.0 | 34.0 | 46.0 | 6.0 | 7.0 | 16.0 | 8.0 | 0.0 | 12.0 | 20.0 |
89a8100eacbffff | 45.0 | 40.0 | 51.0 | 43.0 | 50.0 | 42.0 | 53.0 | 33.0 | 40.0 | 35.0 | ... | 48.0 | 44.0 | 56.0 | 16.0 | 19.0 | 26.0 | 8.0 | 12.0 | 0.0 | 7.0 |
89a8100eadbffff | 53.0 | 47.0 | 58.0 | 50.0 | 57.0 | 50.0 | 61.0 | 41.0 | 48.0 | 42.0 | ... | 55.0 | 52.0 | 64.0 | 24.0 | 26.0 | 34.0 | 15.0 | 20.0 | 7.0 | 0.0 |
323 rows × 323 columns
Explore results#
Travel times from anywhere to a particular place#
Once the travel time matrix is computed, we can use the data to analyse and visualise different measures of accessibility. For instance, we can filter the table to show all rows for which the destination is the Praça da Sé, a public square in the centre of the city. By plotting the travel times in a map, we can quickly assess how long it takes for residents from different parts of the city to reach this square by public transport.
For this we first create a copy of the result data frame, filtered to contain
only rows with to_id
referencing the Praça da Sé. Then, we join this table to
the input hexagonal grid, and drop any records that have NaN
values, i.e., for
which there was no result. Finally, as we did above, we use the
explore()
to display the values in a
map.
PRAÇA_DA_SÉ = "89a8100c02fffff"
travel_times_to_centre = travel_time_matrix[travel_time_matrix["to_id"] == PRAÇA_DA_SÉ].copy()
travel_times_to_centre = travel_times_to_centre.set_index("from_id")[["travel_time"]]
hexagons_with_travel_time_to_centre = (
hexagon_grid.set_index("id").join(travel_times_to_centre)
)
hexagons_with_travel_time_to_centre
population | jobs | schools | geometry | travel_time | |
---|---|---|---|---|---|
id | |||||
89a8100c603ffff | 1146 | 1155 | 0 | POLYGON ((336000.312 7392162.721, 336107.079 7... | 49.0 |
89a8100c617ffff | 700 | 463 | 1 | POLYGON ((335685.425 7392005.022, 335792.187 7... | 47.0 |
89a8100c60fffff | 377 | 257 | 0 | POLYGON ((336005.719 7392502.350, 336112.488 7... | 50.0 |
89a8100c607ffff | 743 | 687 | 0 | POLYGON ((335690.825 7392344.644, 335797.590 7... | 40.0 |
89a8100c6abffff | 601 | 29 | 0 | POLYGON ((335375.940 7392186.943, 335482.700 7... | 54.0 |
... | ... | ... | ... | ... | ... |
89a8100cea7ffff | 34 | 2483 | 0 | POLYGON ((330455.500 7397136.529, 330562.237 7... | 44.0 |
89a8100ce23ffff | 382 | 395 | 1 | POLYGON ((331090.629 7397791.927, 331197.377 7... | 52.0 |
89a8100ce37ffff | 10 | 202 | 0 | POLYGON ((330775.717 7397634.124, 330882.461 7... | 44.0 |
89a8100c8dbffff | 19 | 438 | 0 | POLYGON ((330460.813 7397476.324, 330567.552 7... | 45.0 |
89a8100c8cbffff | 12 | 1375 | 0 | POLYGON ((330466.126 7397816.132, 330572.867 7... | 49.0 |
323 rows × 5 columns
hexagons_with_travel_time_to_centre.explore(
column="travel_time",
cmap="YlOrBr",
tiles="CartoDB.Positron",
)
You can clearly see how travel times do not increase uniformly, but are shorter along the major transport axis (metro, railways, bus corridors).
Aggregated/average accessibility#
Another quick way of getting an understanding of how well different parts of the city are served by public transport is to aggregate the travel times from or to each cell over the entire study region. Of course, this creates edge effects, so in our limited example, grid cells further outside will have worse over-all accessibility values. However, if an entire city region, e.g., covering the entire public transport network, can be captured in one analysis, unwanted artefacts of the analysis have a smaller impact.
To aggregate travel times, we can use the
groupby()
method of pandas’ data frames, and
one of the different aggregation functions available for the resulting
pandas.GroupBy
objects. For instance, to show the median travel time
from any cell to any other cell in our grid, we group the results using
from_id
and median()
:
median_travel_times = travel_time_matrix.groupby("from_id").median("travel_time")
median_travel_times
travel_time | |
---|---|
from_id | |
89a8100c003ffff | 42.0 |
89a8100c007ffff | 37.0 |
89a8100c00bffff | 43.0 |
89a8100c00fffff | 38.0 |
89a8100c013ffff | 47.0 |
... | ... |
89a8100ea4fffff | 38.0 |
89a8100ea53ffff | 40.0 |
89a8100ea5bffff | 36.0 |
89a8100eacbffff | 46.0 |
89a8100eadbffff | 54.0 |
323 rows × 1 columns
Again, we can join these median travel times to the hexagonal grid to display a nice map:
hexagons_with_median_travel_times = (
hexagon_grid.set_index("id").join(median_travel_times)
)
hexagons_with_median_travel_times.explore(
column="travel_time",
cmap="YlOrBr",
tiles="CartoDB.Positron",
)
Bibliography#
Braga, C. K. V., Loureiro, C. F. G., & Pereira, R. H.M. (2023): Evaluating the impact of public transport travel time inaccuracy and variability on socio-spatial inequalities in accessibility. Journal of Transport Geography, 109, 103590. DOI:10.1016/j.jtrangeo.2023.103590
Farber, S. & Fu, L. (2017): Dynamic public transit accessibility using travel time cubes: Comparing the effects of infrastructure (dis)investments over time. Computers, Environment and Urban Systems, 62, 30–40. DOI:10.1016/j.compenvurbsys.2016.10.005
Farber, S., Morang, M. Z., & Widener, M. J. (2014): Temporal variability in transit-based accessibility to supermarkets. Applied Geography, 53, 149–159. DOI:10.1016/j.apgeog.2014.06.012
Herszenhut, D., Pereira, R. H.M., Portugal, L. D. S., & Oliveira, M. H. D. S. (2022): The impact of transit monetary costs on transport inequality. Journal of Transport Geography, 99, 103309. DOI:10.1016/j.jtrangeo.2022.103309
Järv, O., Tenkanen, H., Salonen, M., Ahas, R., & Toivonen, T. (2018): Dynamic cities: Location-based accessibility modelling as a function of time. Applied Geography, 95, 101–110. DOI:10.1016/j.apgeog.2018.04.009
Pereira, R. H. M., Braga, C. K. V., Servo, L. M., Serra, B., Amaral, P., Gouveia, N., & Paez, A. (2021): Geographic access to COVID-19 healthcare in Brazil using a balanced float catchment area approach. Social Science & Medicine, 273. DOI:10.1016/j.socscimed.2021.113773
Salonen, M. & Toivonen, T. (2013): Modelling travel time in urban networks: comparable measures for private car and public transport. Journal of Transport Geography, 31, 143–153. DOI:10.1016/j.jtrangeo.2013.06.011
Tenkanen, H. & Toivonen, T. (2020): Longitudinal spatial dataset on travel times and distances by different travel modes in Helsinki Region. Scientific Data, 7(1), 77. DOI:10.1038/s41597-020-0413-y