Repository: r5py/r5py (language: python)

Issue:
Improve basic tutorial
(This is on a sidenote, and deserves its own issue, maybe)

We should also try to streamline the story line of the basic-usage notebook, I think first having the grid points, then loading the transport network, might be a better idea.

_Originally posted by @christophfink in https://github.com/r5py/r5py/issues/64#issuecomment-1125801130_

In addition, to this ^, it would be very nice to also show something visual from the results. I.e. create a map showing the travel times to/from specific point. 


Discussion / hints:
yes! so, should we still create the same data set with grid cells?
In terms of visuals, I tend to use [Altair](https://altair-viz.github.io/index.html) for basically everything I do - but of course that requires a separate package installation. MPL might be a better way to go?
Also I'd be happy to recrate a dataset for another city (just did one for Calgary) or based on what you had if you wanted to do a hexgrid or something like that.
> I do think it should be set up as a warning/exclamation in the documentation for any "Getting Started" documents, since setting the appropriate memory is sort of a big fundamental choice in terms of performance and experience with the system.
> 
> To play devil's advocate: One advantage of making the user set an explicit memory (or at least requiring some sort of call/action to set it up) is that they are made aware that we are sitting on top of a fairly beefy java machine running underneath. My worry is that the 80%/2GB rule might create some problems down the road that are more slippery to diagnose.

https://github.com/r5py/r5py/issues/35#issuecomment-1128063771 by @wklumpen 

> That’s a very valid point. Let’s add a warning to the `basic-usage.ipynb`, so everybody should have read it.

https://github.com/r5py/r5py/issues/35#issuecomment-1128082355 by @christophfink 
> Also I'd be happy to recrate a dataset for another city (just did one for Calgary) or based on what you had if you wanted to do a hexgrid or something like that.

I guess it would be nice to have another dataset for some of the more advances tutorials we will be writing, but no hurries with this. We’ll also have to make sure that we keep all sample files nice and small (and GTFS are huge!).
> In terms of visuals, I tend to use [Altair](https://altair-viz.github.io/index.html) for basically everything I do - but of course that requires a separate package installation. MPL might be a better way to go?

Never used it TBH. For the time being, I guess we’re fine with what `geopandas` and pandas do by default, but making things look nicer is always great :)
I'll probably put a Calgary-focused post up on the website once we do a launchy-launch thing with some pretty pictures so I can go hogwild then.
Based on our discussion, I propose the following three sections in a basic tutorial:

* A simple "make a travel time matrix from a set of dots" using Helsinki and São Paulo data
* Using the results above to do a basic access calculation using the destination counts for the two data sets
* Using an `expanded_travel_time_matrix` in some way (ideas?)
Who can push forward with this? @HTenkanen ?
@christophfink Yes, I have advanced with this and will now work on this. Aiming to get this done in the coming hours..

Fix patch:
diff --git a/docs/md/basic-usage.md b/docs/md/basic-usage.md
index c35127ea..7d3798a2 100644
--- a/docs/md/basic-usage.md
+++ b/docs/md/basic-usage.md
@@ -14,40 +14,132 @@ jupyter:
 
 # Usage
 
-```python nbsphinx="hidden" tags=["remove-cell"]
-# this cell is hidden from output
-# it’s used to 
-# - set sys.path to point to the local repo
-# - make ipython show all outputs of each cell
+## Introduction
 
-# - set sys.path to point to the local repo
-import pathlib
+**R5py** is a Python library for routing and calculating travel time matrices on multimodal transport networks (walk, bike, public transport and car).
+It provides a simple and friendly interface to R<sup>5</sup> (*the Rapid Realistic Routing on Real-world and Reimagined networks*) which is a [routing engine](https://github.com/conveyal/r5) developed by [Conveyal](https://conveyal.com/). `R5py` is designed to interact with [GeoPandas](https://geopandas.org) GeoDataFrames, and it is inspired by [r5r](https://ipeagit.github.io/r5r) which is a similar wrapper developed for R. `R5py` exposes some of R5’s functionality via its [Python API](reference.html), in a syntax similar to r5r’s. At the time of this writing, only the computation of travel time matrices has been fully implemented. Over time, `r5py` will be expanded to incorporate other functionalities from R5.
+
+<!-- #region -->
+## Data requirements
+
+### Data for creating a routable network
+
+When calculating travel times with `r5py`, you typically need a couple of datasets:
+
+- **A road network dataset from OpenStreetMap** (OSM) in Protocolbuffer Binary (`.pbf`) format:
+  - This data is used for finding the fastest routes and calculating the travel times based on walking, cycling and driving. In addition, this data is used for walking/cycling legs between stops when routing with transit.
+  - *Hint*: Sometimes you might need modify the OSM data beforehand, e.g., by cropping the data or adding special costs for travelling (e.g., for considering slope when cycling/walking). When doing this, you should follow the instructions on the [Conveyal website](https://docs.conveyal.com/prepare-inputs#preparing-the-osm-data). For adding customized costs for pedestrian and cycling analyses, see [this repository](https://github.com/RSGInc/ladot_analysis_dataprep).
+
+- **A transit schedule dataset** in General Transit Feed Specification (GTFS.zip) format (optional):
+   - This data contains all the necessary information for calculating travel times based on public transport, such as stops, routes, trips and the schedules when the vehicles are passing a specific stop. You can read about the [GTFS standard here](https://developers.google.com/transit/gtfs/reference).
+   - *Hint*: `r5py` can also combine multiple GTFS files, as sometimes you might have different GTFS feeds representing, e.g., the bus and metro connections.
+
+
+### Data for origin and destination locations
+
+In addition to OSM and GTFS datasets, you need data that represents the origin and destination locations (OD-data) for routings. This data is typically stored in one of the geospatial data formats, such as Shapefile, GeoJSON or GeoPackage. As `r5py` is built on top of `geopandas`, it is easy to read OD-data from various different data formats.
+
+
+### Where to get these datasets?
+
+Here are a few places from where you can download the datasets for creating the routable network:
+
+- **OpenStreetMap data in PBF-format**:
+
+  - [pyrosm](https://pyrosm.readthedocs.io/en/latest/basics.html#protobuf-file-what-is-it-and-how-to-get-one)  library. Allows downloading data directly from Python (based on GeoFabrik and BBBike).
+  - [pydriosm](https://pydriosm.readthedocs.io/en/latest/quick-start.html#download-data) library. Allows downloading data directly from Python (based on GeoFabrik and BBBike).
+  - [GeoFabrik](http://download.geofabrik.de/) website. Has data extracts for many pre-defined areas (countries, regions, etc).
+  - [BBBike](https://download.bbbike.org/osm/bbbike/) website. Has data extracts readily available for many cities across the world. Also supports downloading data by [specifying your own area or interest](https://extract.bbbike.org/).
+  - [Protomaps](https://protomaps.com/downloads/osm) website. Allows to download the data with custom extent by specifying your own area of interest.
+
+
+- **GTFS data**:
+  - [Transitfeeds](https://transitfeeds.com/) website. Easy to navigate and find GTFS data for different countries and cities. Includes current and historical GTFS data. Notice: The site will be deprecated in the future.
+  - [Mobility Database](https://database.mobilitydata.org) website. Will eventually replace TransitFeeds.
+  - [Transitland](https://www.transit.land/operators) website. Find data based on country, operator or feed name. Includes current and historical GTFS data.
+
+### Sample datasets
+
+In the following tutorial, we use various open source datasets:
+- The point dataset for Helsinki has been obtained from [Helsinki Region Environmental Services](https://www.hsy.fi/en/environmental-information/open-data/avoin-data---sivut/population-grid-of-helsinki-metropolitan-area/) (HSY) licensed under a Creative Commons By Attribution 4.0.
+- The street network for Helsinki is a cropped and filtered extract of OpenStreetMap (© OpenStreetMap contributors, [ODbL license](https://www.openstreetmap.org/copyright))
+- The GTFS transport schedule dataset for Helsinki is a cropped and minimised copy of Helsingin seudun liikenne’s (HSL) open dataset ([Creative Commons BY 4.0](https://www.hsl.fi/hsl/avoin-data#aineistojen-kayttoehdot)).
+<!-- #endregion -->
+
+## Installation
+
+Before you can start using `r5py`, you need to install it and a few libraries. Check [installation instructions](../installation.md) for more details.
+
+
+## Configuring `r5py` before using it
+
+It is possible to configure `r5py` in a few different ways (see [configuration instructions](../configuration.md) for details). One of the options that you most likely want to adjust, is **configuring how much memory** (RAM) `r5py` will consume during the calculations. `r5py` runs a powerful Java engine under the hood, and by default it will use **80 % of the available memory** for doing the calculations. However, you can easily adjust this.
+
+If you want to allocate, e.g., a maximum of 5 Gb of RAM for the tool, you can do so by running:
+
+```python
 import sys
-sys.path.insert(0, str(pathlib.Path().absolute().parent.parent / "src"))
+sys.argv.append(["--max-memory", "5G"])
+```
+
+By running this, `r5py` will use **at maximum** 5 Gb of memory. However, it does not mean that the tool will necessary use all of this memory if it does not need it.
 
-# - make ipython show all outputs of each cell
-get_ipython().ast_node_interactivity = "all"
+```{important}
+Notice that changing the amount of allocated memory should alway be done as the first thing in your script, i.e. it should be run **before** importing `r5py`.
 ```
 
-## Python
 
-R5py exposes some of R5’s functionality via its [Python API](reference.html), in a syntax similar to r5r’s. At the time of this writing, only the computation of travel time matrices has been fully implemented. 
+## Getting started with `r5py`
 
-Below, you find a minimal example of computing a travel time matrix:
+Next, we will learn how to calculate travel times with `r5py` between locations spread around the city center area of Helsinki, Finland.
 
-<!-- #region tags=[] -->
-### Load transport network
+### Load the origin and destination data
+
+Let's start by downloading a sample point dataset into a geopandas `GeoDataFrame` that we can use as our origin and destination locations. For the sake of this exercise, we have prepared a grid of points covering parts of Helsinki. The point data also contains the number of residents of each 250 meter cell:
+
+```python
+import geopandas
+
+points_url = "https://github.com/r5py/r5py/raw/main/docs/data/Helsinki/population_points_2020.gpkg"
+points = geopandas.read_file(points_url)
+points.head()
+```
 
-Virtually all operations of r5py require a transport network. As input files to construct a transport network topology, r5py needs the following data sets:
+The `points` GeoDataFrame contains a few columns, namely `id`, `population` and `geometry`. The `id` column with unique values and `geometry` columns are required for `r5py` to work. If your input point dataset does not have an `id` column with unique values, `r5py` will throw an error.
 
-- The street network in the form of an OpenStreetMap extract covering the study area, prepared according to the instructions at https://docs.conveyal.com/prepare-inputs#preparing-the-osm-data, and possibly annotated with LADOT tags (see https://github.com/RSGInc/ladot_analysis_dataprep).
-- Public transport schedules in GTFS format (optional, r5py can combine multiple GTFS files)
+To get a better sense of the data, let's create a map that shows the locations of the points and visualise the number of people living in each cell (the cells are represented by their centre point):
 
-In this example, we use data from Helsinki metropolitan area, which you can find in the source code repository of r5py in `docs/data/`. The street network is a cropped and filtered extract of OpenStreetMap (© OpenStreetMap contributors, [ODbL license](https://www.openstreetmap.org/copyright)), the GTFS transport schedule is a cropped and minimised copy of Helsingin seudun liikenne’s (HSL) open data set [Creative Commons BY 4.0](https://www.hsl.fi/hsl/avoin-data#aineistojen-kayttoehdot).
+```python
+points.explore("population", cmap="Reds", marker_kwds={"radius": 12})
+```
+
+Let's pick one of these points to represent our **origin** and store it in a separate GeoDataFrame:
+
+```python
+origin = points.loc[points["id"] == 54].copy()
+origin.explore(color="blue", max_zoom=14, marker_kwds={"radius": 12})
+```
 
-To import the street and public transport networks, instantiate an `r5py.TransportNetwork` with the file paths to the OSM extract and the GTFS files:
+<!-- #region tags=[] -->
+### Load transport network
+
+Virtually all operations of `r5py` require a transport network. In this example, we use data from Helsinki metropolitan area, which you can find in the source code repository of r5py in `docs/data/` [(see here)](https://github.com/r5py/r5py/tree/main/docs/data). To import the street and public transport networks, instantiate an `r5py.TransportNetwork` with the file paths to the OSM extract and the GTFS files:
 <!-- #endregion -->
 
+```python nbsphinx="hidden" tags=["remove-cell"]
+# this cell is hidden from output
+# it’s used to set sys.path to point to the local repo
+import pathlib
+import sys
+sys.path.insert(0, str(pathlib.Path().absolute().parent.parent / "src"))
+```
+
+```python
+# Allow 8 GB of memory
+import sys
+sys.argv.append(["--max-memory", "8G"])
+```
+
 ```python
 from r5py import TransportNetwork
 
@@ -59,58 +151,165 @@ transport_network = TransportNetwork(
 )
 ```
 
-### Compute a travel time matrix
+At this stage, `r5py` has created the routable transport network and it is stored in the `transport_network` variable. We can now start using this network for doing the travel time calculations.
 
-A travel time matrix is a data set detailing the travel costs (e.g., time) from each point to each point in a set of origins and destinations in a study area. 
 
-For the sake of this exercise, we have prepared a grid of points covering parts of Helsinki. The point dataset has been obtained from [Helsinki Region Environmental Services](https://www.hsy.fi/en/environmental-information/open-data/avoin-data---sivut/population-grid-of-helsinki-metropolitan-area/) (HSY) licensed under a Creative Commons By Attribution 4.0. The point data also contains information about residents of each 250 meter cell.
+### Compute travel time matrix from one to all locations
+
+A travel time matrix is a dataset detailing the travel costs (e.g., time) between given locations (origins and destinations) in a study area. To compute a travel time matrix with `r5py` based on public transportation, we first need to initialize an `r5py.TravelTimeMatrixComputer` object. As inputs, we pass following arguments for the `TravelTimeMatrixComputer`:
+- `transport_network`, which we created in the previous step representing the routable transport network.
+- `origins`, which is a GeoDataFrame with one location that we created earlier (however, you can also use multiple locations as origins).
+- `destinations`, which is a GeoDataFrame representing the destinations (in our case, the `points` GeoDataFrame).
+- `departure`, which should be Python's `datetime` object (in our case standing for "22nd of February 2022 at 08:30") to tell `r5py` that the schedules of this specific time and day should be used for doing the calculations.
+   - *Note*: By default, `r5py` summarizes and calculates a median travel time from all possible connections within one hour from given depature time (with 1 minute frequency). It is possible to adjust this time window using `departure_time_window` parameter ([see details here]((https://r5py.readthedocs.io/en/stable/reference.html#r5py.RegionalTask))).
+- `transport_modes`, which determines the travel modes that will be used in the calculations. These can be passed using the options from the `TransitMode` and `LegMode` classes.
+  - *Hint*: To see all available options, run `help(TransitMode)` or `help(LegMode)`.
+
+```{note} In addition to these ones, the constructor also accepts many other parameters [listed here](https://r5py.readthedocs.io/en/stable/reference.html#r5py.RegionalTask), such as walking and cycling speed, maximum trip duration, maximum number of transit connections used during the trip, etc.
+```
+
+Now, we will first create a `travel_time_matrix_computer` instance as described above:
 
 ```python
-import geopandas
+import datetime
+from r5py import TravelTimeMatrixComputer, TransitMode, LegMode
 
-grid_points = geopandas.read_file("../data/Helsinki/population_points_2020.gpkg")
 
-grid_points.head()
+travel_time_matrix_computer = TravelTimeMatrixComputer(
+    transport_network,
+    origins=origin,
+    destinations=points,
+    departure=datetime.datetime(2022,2,22,8,30),
+    transport_modes=[TransitMode.TRANSIT, LegMode.WALK]
+)
+
 ```
 
+Running this initializes the `TravelTimeMatrixComputer`, but any calculations were not done yet.
+To actually run the computations, we need to call `.compute_travel_times()` on the instance, which will calculate the travel times between all points:
+
+```python
+travel_time_matrix = travel_time_matrix_computer.compute_travel_times()
+travel_time_matrix.head()
+```
 
-We can now visualise the number of people living in each cell (the cells are represented by their centre point):
+As a result, this returns a `pandas.DataFrame` which we stored in the `travel_time_matrix` variable. The values in the `travel_time` column are travel times in minutes between the points identified by `from_id` and `to_id`. As you can see, the `id` value in the `from_id` column is the same for all rows because we only used one origin location as input.
 
+To get a better sense of the results, let's create a travel time map based on our results. We can do this easily by making a table join between the `points` GeoDataFrame and the `travel_time_matrix`. The key in the `travel_time_matrix` table is the column `to_id` and the corresponding key in `points` GeoDataFrame is the column `id`:
 
 ```python
-grid_points.explore("population", cmap="Reds", marker_kwds={"radius": 12})
+join = points.merge(travel_time_matrix, left_on="id", right_on="to_id")
+join.head()
 ```
 
-Now, to compute a travel time matrix between all `grid_points`, we first instantiate an `r5py.TravelTimeMatrixComputer` with the transport network we created earlier, and a list of origins (since we don’t specify a separate set of destinations, r5py will use the same points as destinations and produce a full set of origins and destinations). The constructor also accepts all parameters of [RegionalTask](reference.html#r5py.RegionalTask), such as transport modes, or walking speed. 
-
-Calling `compute_travel_times()` on the instance will return a `pandas.DataFrame` with travel times between all points.
+Now we have the travel times attached to each point, and we can easily visualize them on a map:
 
 ```python
-import datetime
+join.explore("travel_time", cmap="Greens", marker_kwds={"radius": 12})
+```
 
-from r5py import TravelTimeMatrixComputer, TransitMode, LegMode
+### Compute travel time matrix from all to all locations
+
+Running the calculations between all points in our sample dataset can be done in a similar manner as calculating the travel times from one origin to all destinations.
+Since, calculating these kind of all-to-all travel time matrices is quite typical when doing accessibility analyses, it is actually possible to calculate a cross-product between all points just by using the `origins` parameter (i.e. without needing to specify a separate set for destinations). `r5py` will use the same points as destinations and produce a full set of origins and destinations:
 
 
+```python
 travel_time_matrix_computer = TravelTimeMatrixComputer(
     transport_network,
-    origins=grid_points,
+    origins=points,
     departure=datetime.datetime(2022,2,22,8,30),
-    transport_modes=[TransitMode.TRANSIT, LegMode.WALK],
-    #percentiles=[1,25,50,75,99],
-    #breakdown=True
+    transport_modes=[TransitMode.TRANSIT, LegMode.WALK]
 )
-travel_time_matrix = travel_time_matrix_computer.compute_travel_times()
-travel_time_matrix
+travel_time_matrix_all = travel_time_matrix_computer.compute_travel_times()
+travel_time_matrix_all.head()
 ```
 
-The values in the `travel_time` column are travel times between the points identified by `from_id` and `to_id`, in minutes. 
+```python
+travel_time_matrix_all.tail()
+```
 
-The median travel time to or from a certain point gives a good estimate of the overall accessibility to or from it, in relation to other points:
+```python
+len(travel_time_matrix_all)
+```
+
+As we can see from the outputs above, now we have calculated travel times between all points (n=92) in the study area. Hence, the resulting DataFrame has almost 8500 rows (92x92=8464). Based on these results, we can for example calculate the median travel time to or from a certain point, which gives a good estimate of the overall accessibility of the location in relation to other points:
 
 ```python tags=[]
-travel_time_matrix.groupby("from_id")["travel_time"].median()
+median_times = travel_time_matrix_all.groupby("from_id")["travel_time"].median()
+median_times
+```
+
+To estimate, how long does it take in general to travel between locations in our study area (i.e. what is the baseline accessibility in the area), we can calculate the mean (or median) of the median travel times showing that it is approximately 22 minutes:
+
+```python
+median_times.mean()
+```
+
+Naturally, we can also visualize these values on a map:
+
+```python
+overall_access = points.merge(median_times.reset_index(), left_on="id", right_on="from_id")
+overall_access.head()
+```
+
+```python
+overall_access.explore("travel_time", cmap="Blues", scheme="natural_breaks", k=4, marker_kwds={"radius": 12})
 ```
 
+In our study area, there seems to be a bit poorer accessibility in the Southern areas and on the edges of the region (i.e. we witness a classic edge-effect here).
+
+
+## Advanced usage
+
+### Compute travel times with a detailed breakdown of the routing results
+
+
+In case you are interested in more detailed routing results, it is possible to specify `breakdown=True` once initializing the `TravelTimeMatrixComputer` object. This will provide not only the same information as in the previous examples, but it also brings more detailed information about the routings. When breakdown is enabled, `r5py` produces information about the used routes for each origin-destination pair, as well as total time disaggregated by access, waiting, in-vehicle and transfer times:
+
 ```python
-travel_time_matrix.groupby("from_id")["travel_time"].median().median()
+travel_time_matrix_computer = TravelTimeMatrixComputer(
+    transport_network,
+    origins=origin,
+    destinations=points,
+    departure=datetime.datetime(2022,2,22,8,30),
+    transport_modes=[TransitMode.TRANSIT, LegMode.WALK],
+    breakdown=True,
+)
+travel_time_matrix_detailed = travel_time_matrix_computer.compute_travel_times()
+travel_time_matrix_detailed.head()
+```
+
+As you can see, the result contains much more information than earlier, see the following table for explanations:
+
+| Column        | Description                                                          | Data type |
+| ------------- | -------------------------------------------------------------------- | --------- |
+| **routes**        | The route-ids (lines) used during the trip                           | list      |
+| **board_stops**   | The stop-ids of the boarding stops                                   | list      |
+| **alight_stops**  | The stop-ids of the alighting stops                                  | list      |
+| **ride_times**    | In vehicle ride times of individual journey legs                     | list      |
+| **access_time**   | The time it takes for the "first mile" of a trip                     | float     |
+| **egress_time**   | The time it takes for the "last mile" of a trip                      | float     |
+| **transfer_time** | The time it takes to transfer from vechile to another                | float     |
+| **wait_times**    | The time(s) it take to wait for the vehicle at a stop                | list      |
+| **total_time**    | Sum(ride_times, access_time, egress_time, transfer_time, wait_times) | float     |
+| **n_iterations**  | Number of iterations used for calculating the travel times           | int       |
+
+
+
+### Compute travel times for different percentiles
+
+Because `r5py` calculates travel times for all possible transit departure possibilities within an hour (with one minute frequency), we basically get a distribution of travel times. It is possible to gather and return information about the travel times at different percentiles of this distribution based on all computed trips (sorted from the fastest to slowest connections). By default, the returned time in `r5py` is the median travel time (i.e. `50`). You can access these percentiles by using a parameter `percentiles` which accepts a list of integers representing different percentiles, such as `[25, 50, 75]` which returns the travel times at those percentiles:
+
+```python
+travel_time_matrix_computer = TravelTimeMatrixComputer(
+    transport_network,
+    origins=origin,
+    destinations=points,
+    departure=datetime.datetime(2022,2,22,8,30),
+    transport_modes=[TransitMode.TRANSIT, LegMode.WALK],
+    percentiles=[25, 50, 75],
+)
+travel_time_matrix_detailed = travel_time_matrix_computer.compute_travel_times()
+travel_time_matrix_detailed.head()
 ```
diff --git a/docs/notebooks/basic-usage.ipynb b/docs/notebooks/basic-usage.ipynb
index 0c8d59b2..ca72c08e 100644
--- a/docs/notebooks/basic-usage.ipynb
+++ b/docs/notebooks/basic-usage.ipynb
@@ -5,38 +5,175 @@
    "id": "f1c4253e",
    "metadata": {},
    "source": [
-    "# Usage"
+    "# Usage\n",
+    "\n",
+    "## Introduction\n",
+    "\n",
+    "**R5py** is a Python library for routing and calculating travel time matrices on multimodal transport networks (walk, bike, public transport and car).\n",
+    "It provides a simple and friendly interface to R<sup>5</sup> (*the Rapid Realistic Routing on Real-world and Reimagined networks*) which is a [routing engine](https://github.com/conveyal/r5) developed by [Conveyal](https://conveyal.com/). `R5py` is designed to interact with [GeoPandas](https://geopandas.org) GeoDataFrames, and it is inspired by [r5r](https://ipeagit.github.io/r5r) which is a similar wrapper developed for R. `R5py` exposes some of R5’s functionality via its [Python API](reference.html), in a syntax similar to r5r’s. At the time of this writing, only the computation of travel time matrices has been fully implemented. Over time, `r5py` will be expanded to incorporate other functionalities from R5. "
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "af121919-dadf-44ae-8e49-9518049663ee",
+   "metadata": {},
+   "source": [
+    "## Data requirements\n",
+    "\n",
+    "### Data for creating a routable network\n",
+    "\n",
+    "When calculating travel times with `r5py`, you typically need a couple of datasets: \n",
+    "\n",
+    "- **A road network dataset from OpenStreetMap** (OSM) in Protocolbuffer Binary (`.pbf`) format: \n",
+    "  - This data is used for finding the fastest routes and calculating the travel times based on walking, cycling and driving. In addition, this data is used for walking/cycling legs between stops when routing with transit. \n",
+    "  - *Hint*: Sometimes you might need modify the OSM data beforehand, e.g., by cropping the data or adding special costs for travelling (e.g., for considering slope when cycling/walking). When doing this, you should follow the instructions on the [Conveyal website](https://docs.conveyal.com/prepare-inputs#preparing-the-osm-data). For adding customized costs for pedestrian and cycling analyses, see [this repository](https://github.com/RSGInc/ladot_analysis_dataprep).\n",
+    "\n",
+    "- **A transit schedule dataset** in General Transit Feed Specification (GTFS.zip) format (optional):\n",
+    "   - This data contains all the necessary information for calculating travel times based on public transport, such as stops, routes, trips and the schedules when the vehicles are passing a specific stop. You can read about the [GTFS standard here](https://developers.google.com/transit/gtfs/reference).\n",
+    "   - *Hint*: `r5py` can also combine multiple GTFS files, as sometimes you might have different GTFS feeds representing, e.g., the bus and metro connections. \n",
+    "\n",
+    "\n",
+    "### Data for origin and destination locations\n",
+    "\n",
+    "In addition to OSM and GTFS datasets, you need data that represents the origin and destination locations (OD-data) for routings. This data is typically stored in one of the geospatial data formats, such as Shapefile, GeoJSON or GeoPackage. As `r5py` is built on top of `geopandas`, it is easy to read OD-data from various different data formats. \n",
+    "\n",
+    "\n",
+    "### Where to get these datasets?\n",
+    "\n",
+    "Here are a few places from where you can download the datasets for creating the routable network:\n",
+    "\n",
+    "- **OpenStreetMap data in PBF-format**:\n",
+    "\n",
+    "  - [pyrosm](https://pyrosm.readthedocs.io/en/latest/basics.html#protobuf-file-what-is-it-and-how-to-get-one)  library. Allows downloading data directly from Python (based on GeoFabrik and BBBike).\n",
+    "  - [pydriosm](https://pydriosm.readthedocs.io/en/latest/quick-start.html#download-data) library. Allows downloading data directly from Python (based on GeoFabrik and BBBike).\n",
+    "  - [GeoFabrik](http://download.geofabrik.de/) website. Has data extracts for many pre-defined areas (countries, regions, etc).\n",
+    "  - [BBBike](https://download.bbbike.org/osm/bbbike/) website. Has data extracts readily available for many cities across the world. Also supports downloading data by [specifying your own area or interest](https://extract.bbbike.org/).\n",
+    "  - [Protomaps](https://protomaps.com/downloads/osm) website. Allows to download the data with custom extent by specifying your own area of interest.\n",
+    "\n",
+    "\n",
+    "- **GTFS data**:  \n",
+    "  - [Transitfeeds](https://transitfeeds.com/) website. Easy to navigate and find GTFS data for different countries and cities. Includes current and historical GTFS data. Notice: The site will be deprecated in the future.  \n",
+    "  - [Mobility Database](https://database.mobilitydata.org) website. Will eventually replace TransitFeeds. \n",
+    "  - [Transitland](https://www.transit.land/operators) website. Find data based on country, operator or feed name. Includes current and historical GTFS data.\n",
+    "\n",
+    "### Sample datasets\n",
+    "\n",
+    "In the following tutorial, we use various open source datasets:\n",
+    "- The point dataset for Helsinki has been obtained from [Helsinki Region Environmental Services](https://www.hsy.fi/en/environmental-information/open-data/avoin-data---sivut/population-grid-of-helsinki-metropolitan-area/) (HSY) licensed under a Creative Commons By Attribution 4.0. \n",
+    "- The street network for Helsinki is a cropped and filtered extract of OpenStreetMap (© OpenStreetMap contributors, [ODbL license](https://www.openstreetmap.org/copyright))\n",
+    "- The GTFS transport schedule dataset for Helsinki is a cropped and minimised copy of Helsingin seudun liikenne’s (HSL) open dataset ([Creative Commons BY 4.0](https://www.hsl.fi/hsl/avoin-data#aineistojen-kayttoehdot))."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "cd6c9d3c-8eb9-499f-b2fa-0dc8761e26da",
+   "metadata": {},
+   "source": [
+    "## Installation\n",
+    "\n",
+    "Before you can start using `r5py`, you need to install it and a few libraries. Check [installation instructions](../installation.md) for more details. "
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "809d5171-6536-43d3-aa20-f3f3a14aa502",
+   "metadata": {},
+   "source": [
+    "## Configuring `r5py` before using it\n",
+    "\n",
+    "It is possible to configure `r5py` in a few different ways (see [configuration instructions](../configuration.md) for details). One of the options that you most likely want to adjust, is **configuring how much memory** (RAM) `r5py` will consume during the calculations. `r5py` runs a powerful Java engine under the hood, and by default it will use **80 % of the available memory** for doing the calculations. However, you can easily adjust this. \n",
+    "\n",
+    "If you want to allocate, e.g., a maximum of 5 Gb of RAM for the tool, you can do so by running:"
    ]
   },
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "439ee015",
-   "metadata": {
-    "nbsphinx": "hidden",
-    "tags": [
-     "remove-cell"
-    ]
-   },
+   "id": "d637c51a-eeed-400b-a625-5c4745db92f4",
+   "metadata": {},
    "outputs": [],
    "source": [
-    "# this cell is hidden from output\n",
-    "# it’s used to set sys.path to point to the local repo\n",
-    "import pathlib\n",
     "import sys\n",
-    "sys.path.insert(0, str(pathlib.Path().absolute().parent.parent / \"src\"))"
+    "sys.argv.append([\"--max-memory\", \"5G\"])"
    ]
   },
   {
    "cell_type": "markdown",
-   "id": "cd224d77",
+   "id": "f0925ade-74ae-4aaf-bfc2-e23fdd9c3ac8",
    "metadata": {},
    "source": [
-    "## Python\n",
+    "By running this, `r5py` will use **at maximum** 5 Gb of memory. However, it does not mean that the tool will necessary use all of this memory if it does not need it. \n",
     "\n",
-    "R5py exposes some of R5’s functionality via its [Python API](reference.html), in a syntax similar to r5r’s. At the time of this writing, only the computation of travel time matrices has been fully implemented. \n",
+    "```{important} \n",
+    "Notice that changing the amount of allocated memory should alway be done as the first thing in your script, i.e. it should be run **before** importing `r5py`. \n",
+    "```"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "a8e9dc27-2585-4559-808a-49e9ee44212a",
+   "metadata": {},
+   "source": [
+    "## Getting started with `r5py`\n",
+    "\n",
+    "Next, we will learn how to calculate travel times with `r5py` between locations spread around the city center area of Helsinki, Finland. \n",
+    "\n",
+    "### Load the origin and destination data\n",
+    "\n",
+    "Let's start by downloading a sample point dataset into a geopandas `GeoDataFrame` that we can use as our origin and destination locations. For the sake of this exercise, we have prepared a grid of points covering parts of Helsinki. The point data also contains the number of residents of each 250 meter cell:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "5c4701cf-31ce-4276-adb5-047c4e36af48",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import geopandas \n",
+    "\n",
+    "points_url = \"https://github.com/r5py/r5py/raw/main/docs/data/Helsinki/population_points_2020.gpkg\"\n",
+    "points = geopandas.read_file(points_url)\n",
+    "points.head()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "7cd4d1c3-73eb-488a-820b-086abb645de7",
+   "metadata": {},
+   "source": [
+    "The `points` GeoDataFrame contains a few columns, namely `id`, `population` and `geometry`. The `id` column with unique values and `geometry` columns are required for `r5py` to work. If your input point dataset does not have an `id` column with unique values, `r5py` will throw an error. \n",
     "\n",
-    "Below, you find a minimal example of computing a travel time matrix:"
+    "To get a better sense of the data, let's create a map that shows the locations of the points and visualise the number of people living in each cell (the cells are represented by their centre point):"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "8b08d5b4-a71d-46ea-a444-19751ed1c64c",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "points.explore(\"population\", cmap=\"Reds\", marker_kwds={\"radius\": 12})"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "9d8651ef-95b6-44e4-a1b6-f43159d8d3b4",
+   "metadata": {},
+   "source": [
+    "Let's pick one of these points to represent our **origin** and store it in a separate GeoDataFrame:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "41f07d2d-60f8-4060-a640-142f7603fb58",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "origin = points.loc[points[\"id\"] == 54].copy()\n",
+    "origin.explore(color=\"blue\", max_zoom=14, marker_kwds={\"radius\": 12})"
    ]
   },
   {
@@ -48,14 +185,38 @@
    "source": [
     "### Load transport network\n",
     "\n",
-    "Virtually all operations of r5py require a transport network. As input files to construct a transport network topology, r5py needs the following data sets:\n",
-    "\n",
-    "- The street network in the form of an OpenStreetMap extract covering the study area, prepared according to the instructions at https://docs.conveyal.com/prepare-inputs#preparing-the-osm-data, and possibly annotated with LADOT tags (see https://github.com/RSGInc/ladot_analysis_dataprep).\n",
-    "- Public transport schedules in GTFS format (optional, r5py can combine multiple GTFS files)\n",
-    "\n",
-    "In this example, we use data from Helsinki metropolitan area, which you can find in the source code repository of r5py in `docs/data/`. The street network is a cropped and filtered extract of OpenStreetMap (© OpenStreetMap contributors, [ODbL license](https://www.openstreetmap.org/copyright)), the GTFS transport schedule is a cropped and minimised copy of Helsingin seudun liikenne’s (HSL) open data set [Creative Commons BY 4.0](https://www.hsl.fi/hsl/avoin-data#aineistojen-kayttoehdot).\n",
-    "\n",
-    "To import the street and public transport networks, instantiate an `r5py.TransportNetwork` with the file paths to the OSM extract and the GTFS files:"
+    "Virtually all operations of `r5py` require a transport network. In this example, we use data from Helsinki metropolitan area, which you can find in the source code repository of r5py in `docs/data/` [(see here)](https://github.com/r5py/r5py/tree/main/docs/data). To import the street and public transport networks, instantiate an `r5py.TransportNetwork` with the file paths to the OSM extract and the GTFS files:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "439ee015",
+   "metadata": {
+    "nbsphinx": "hidden",
+    "tags": [
+     "remove-cell"
+    ]
+   },
+   "outputs": [],
+   "source": [
+    "# this cell is hidden from output\n",
+    "# it’s used to set sys.path to point to the local repo\n",
+    "import pathlib\n",
+    "import sys\n",
+    "sys.path.insert(0, str(pathlib.Path().absolute().parent.parent / \"src\"))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "fadb5a78-b00d-4932-a531-8511c7b931b9",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Allow 8 GB of memory\n",
+    "import sys\n",
+    "sys.argv.append([\"--max-memory\", \"8G\"])"
    ]
   },
   {
@@ -75,92 +236,169 @@
     ")"
    ]
   },
+  {
+   "cell_type": "markdown",
+   "id": "5c6acb2c-89bb-4aab-8697-f8e9c676baf7",
+   "metadata": {},
+   "source": [
+    "At this stage, `r5py` has created the routable transport network and it is stored in the `transport_network` variable. We can now start using this network for doing the travel time calculations. "
+   ]
+  },
   {
    "cell_type": "markdown",
    "id": "d7b1101f-1f01-462a-9ee2-aa3522d0bdd5",
    "metadata": {},
    "source": [
-    "### Compute a travel time matrix\n",
+    "### Compute travel time matrix from one to all locations\n",
     "\n",
-    "A travel time matrix is a data set detailing the travel costs (e.g., time) from each point to each point in a set of origins and destinations in a study area. \n",
+    "A travel time matrix is a dataset detailing the travel costs (e.g., time) between given locations (origins and destinations) in a study area. To compute a travel time matrix with `r5py` based on public transportation, we first need to initialize an `r5py.TravelTimeMatrixComputer` object. As inputs, we pass following arguments for the `TravelTimeMatrixComputer`:\n",
+    "- `transport_network`, which we created in the previous step representing the routable transport network. \n",
+    "- `origins`, which is a GeoDataFrame with one location that we created earlier (however, you can also use multiple locations as origins).\n",
+    "- `destinations`, which is a GeoDataFrame representing the destinations (in our case, the `points` GeoDataFrame). \n",
+    "- `departure`, which should be Python's `datetime` object (in our case standing for \"22nd of February 2022 at 08:30\") to tell `r5py` that the schedules of this specific time and day should be used for doing the calculations. \n",
+    "   - *Note*: By default, `r5py` summarizes and calculates a median travel time from all possible connections within one hour from given depature time (with 1 minute frequency). It is possible to adjust this time window using `departure_time_window` parameter ([see details here]((https://r5py.readthedocs.io/en/stable/reference.html#r5py.RegionalTask))). \n",
+    "- `transport_modes`, which determines the travel modes that will be used in the calculations. These can be passed using the options from the `TransitMode` and `LegMode` classes. \n",
+    "  - *Hint*: To see all available options, run `help(TransitMode)` or `help(LegMode)`.  \n",
     "\n",
-    "For the sake of this exercise, we have prepared a grid of points covering parts of Helsinki. The point dataset has been obtained from [Helsinki Region Environmental Services](https://www.hsy.fi/en/environmental-information/open-data/avoin-data---sivut/population-grid-of-helsinki-metropolitan-area/) (HSY) licensed under a Creative Commons By Attribution 4.0. The point data also contains information about residents of each 250 meter cell."
+    "```{note} In addition to these ones, the constructor also accepts many other parameters [listed here](https://r5py.readthedocs.io/en/stable/reference.html#r5py.RegionalTask), such as walking and cycling speed, maximum trip duration, maximum number of transit connections used during the trip, etc. \n",
+    "```\n",
+    "\n",
+    "Now, we will first create a `travel_time_matrix_computer` instance as described above:"
    ]
   },
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "e7ab7dde-aebb-483f-974b-d0382a5ab7c1",
+   "id": "b5dabcc1-95a4-4c9f-86ab-533d2c752716",
    "metadata": {},
    "outputs": [],
    "source": [
-    "import geopandas\n",
+    "import datetime\n",
+    "from r5py import TravelTimeMatrixComputer, TransitMode, LegMode\n",
     "\n",
-    "grid_points = geopandas.read_file(\"../data/Helsinki/population_points_2020.gpkg\")\n",
     "\n",
-    "grid_points.head()"
+    "travel_time_matrix_computer = TravelTimeMatrixComputer(\n",
+    "    transport_network,\n",
+    "    origins=origin,\n",
+    "    destinations=points,\n",
+    "    departure=datetime.datetime(2022,2,22,8,30),\n",
+    "    transport_modes=[TransitMode.TRANSIT, LegMode.WALK]\n",
+    ")\n"
    ]
   },
   {
    "cell_type": "markdown",
-   "id": "0cc4b64d",
+   "id": "a9b8e76f-ea2c-40bd-b55c-4d9d635c336c",
    "metadata": {},
    "source": [
-    "We can now visualise the number of people living in each cell (the cells are represented by their centre point):\n"
+    "Running this initializes the `TravelTimeMatrixComputer`, but any calculations were not done yet.\n",
+    "To actually run the computations, we need to call `.compute_travel_times()` on the instance, which will calculate the travel times between all points:"
    ]
   },
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "66d5a28e-63cb-4bf0-9e2e-f064f5d216d5",
+   "id": "d7dbed21-6c3c-41be-8163-03674eb03641",
    "metadata": {},
    "outputs": [],
    "source": [
-    "grid_points.explore(\"population\", cmap=\"Reds\", marker_kwds={\"radius\": 12})"
+    "travel_time_matrix = travel_time_matrix_computer.compute_travel_times()\n",
+    "travel_time_matrix.head()"
    ]
   },
   {
    "cell_type": "markdown",
-   "id": "3f1cdb94-b40f-4b83-8be2-a813b2833fef",
+   "id": "533e8704-3fa6-4883-8e2f-093403bb93b0",
    "metadata": {},
    "source": [
-    "Now, to compute a travel time matrix between all `grid_points`, we first instantiate an `r5py.TravelTimeMatrixComputer` with the transport network we created earlier, and a list of origins (since we don’t specify a separate set of destinations, r5py will use the same points as destinations and produce a full set of origins and destinations). The constructor also accepts all parameters of [RegionalTask](reference.html#r5py.RegionalTask), such as transport modes, or walking speed. \n",
+    "As a result, this returns a `pandas.DataFrame` which we stored in the `travel_time_matrix` variable. The values in the `travel_time` column are travel times in minutes between the points identified by `from_id` and `to_id`. As you can see, the `id` value in the `from_id` column is the same for all rows because we only used one origin location as input. \n",
     "\n",
-    "Calling `compute_travel_times()` on the instance will return a `pandas.DataFrame` with travel times between all points."
+    "To get a better sense of the results, let's create a travel time map based on our results. We can do this easily by making a table join between the `points` GeoDataFrame and the `travel_time_matrix`. The key in the `travel_time_matrix` table is the column `to_id` and the corresponding key in `points` GeoDataFrame is the column `id`:"
    ]
   },
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "b5dabcc1-95a4-4c9f-86ab-533d2c752716",
+   "id": "9945deb5-bd5a-4110-a692-5be85b8e8d51",
    "metadata": {},
    "outputs": [],
    "source": [
-    "import datetime\n",
-    "\n",
-    "from r5py import TravelTimeMatrixComputer, TransitMode, LegMode\n",
-    "\n",
+    "join = points.merge(travel_time_matrix, left_on=\"id\", right_on=\"to_id\")\n",
+    "join.head()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "38d0b6ea-a729-435a-bed6-ec69596492e1",
+   "metadata": {},
+   "source": [
+    "Now we have the travel times attached to each point, and we can easily visualize them on a map:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "1fe2cadc-41d7-46f7-b1b6-b6520cb3123b",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "join.explore(\"travel_time\", cmap=\"Greens\", marker_kwds={\"radius\": 12})"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "3f1cdb94-b40f-4b83-8be2-a813b2833fef",
+   "metadata": {},
+   "source": [
+    "### Compute travel time matrix from all to all locations\n",
     "\n",
+    "Running the calculations between all points in our sample dataset can be done in a similar manner as calculating the travel times from one origin to all destinations. \n",
+    "Since, calculating these kind of all-to-all travel time matrices is quite typical when doing accessibility analyses, it is actually possible to calculate a cross-product between all points just by using the `origins` parameter (i.e. without needing to specify a separate set for destinations). `r5py` will use the same points as destinations and produce a full set of origins and destinations:\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "7eb531ce-0b03-41c6-ada6-bd8965d3a10d",
+   "metadata": {},
+   "outputs": [],
+   "source": [
     "travel_time_matrix_computer = TravelTimeMatrixComputer(\n",
     "    transport_network,\n",
-    "    origins=grid_points,\n",
+    "    origins=points,\n",
     "    departure=datetime.datetime(2022,2,22,8,30),\n",
-    "    transport_modes=[TransitMode.TRANSIT, LegMode.WALK],\n",
-    "    #percentiles=[1,25,50,75,99],\n",
-    "    #breakdown=True\n",
+    "    transport_modes=[TransitMode.TRANSIT, LegMode.WALK]\n",
     ")\n",
-    "travel_time_matrix = travel_time_matrix_computer.compute_travel_times()\n",
-    "travel_time_matrix"
+    "travel_time_matrix_all = travel_time_matrix_computer.compute_travel_times()\n",
+    "travel_time_matrix_all.head()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "17dda2cb-cee0-4a94-9c01-df79742d60fb",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "travel_time_matrix_all.tail()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "9536217c-636d-4c72-b0db-b58d48f2c8bf",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "len(travel_time_matrix_all)"
    ]
   },
   {
    "cell_type": "markdown",
-   "id": "4a837f5b-1269-47eb-aa53-5329fb800b90",
+   "id": "99a494be-0eae-457b-9b70-d0c499e193bc",
    "metadata": {},
    "source": [
-    "The values in the `travel_time` column are travel times between the points identified by `from_id` and `to_id`, in minutes. \n",
-    "\n",
-    "The median travel time to or from a certain point gives a good estimate of the overall accessibility to or from it, in relation to other points:"
+    "As we can see from the outputs above, now we have calculated travel times between all points (n=92) in the study area. Hence, the resulting DataFrame has almost 8500 rows (92x92=8464). Based on these results, we can for example calculate the median travel time to or from a certain point, which gives a good estimate of the overall accessibility of the location in relation to other points:"
    ]
   },
   {
@@ -172,7 +410,16 @@
    },
    "outputs": [],
    "source": [
-    "travel_time_matrix.groupby(\"from_id\")[\"travel_time\"].median()"
+    "median_times = travel_time_matrix_all.groupby(\"from_id\")[\"travel_time\"].median()\n",
+    "median_times"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "e687ac07-0d00-4b5b-ac35-d35f9df2b141",
+   "metadata": {},
+   "source": [
+    "To estimate, how long does it take in general to travel between locations in our study area (i.e. what is the baseline accessibility in the area), we can calculate the mean (or median) of the median travel times showing that it is approximately 22 minutes:"
    ]
   },
   {
@@ -182,7 +429,131 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "travel_time_matrix.groupby(\"from_id\")[\"travel_time\"].median().median()"
+    "median_times.mean()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "b180a36f-6cf5-4640-9e37-3d2595473dbd",
+   "metadata": {},
+   "source": [
+    "Naturally, we can also visualize these values on a map:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "7b266697-bf4f-4009-8c9b-2c10c81b111a",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "overall_access = points.merge(median_times.reset_index(), left_on=\"id\", right_on=\"from_id\")\n",
+    "overall_access.head()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "b3eb8010-8952-46ec-bbf8-025f2acb3843",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "overall_access.explore(\"travel_time\", cmap=\"Blues\", scheme=\"natural_breaks\", k=4, marker_kwds={\"radius\": 12})"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "6574f358-5182-4dea-81fb-46e192f34d38",
+   "metadata": {},
+   "source": [
+    "In our study area, there seems to be a bit poorer accessibility in the Southern areas and on the edges of the region (i.e. we witness a classic edge-effect here). "
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "8048ff99-b9d4-4c85-841f-739533a6693f",
+   "metadata": {},
+   "source": [
+    "## Advanced usage\n",
+    "\n",
+    "### Compute travel times with a detailed breakdown of the routing results"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "20a5ec17-ec4c-4085-b690-547fe8b80c0d",
+   "metadata": {},
+   "source": [
+    "In case you are interested in more detailed routing results, it is possible to specify `breakdown=True` once initializing the `TravelTimeMatrixComputer` object. This will provide not only the same information as in the previous examples, but it also brings more detailed information about the routings. When breakdown is enabled, `r5py` produces information about the used routes for each origin-destination pair, as well as total time disaggregated by access, waiting, in-vehicle and transfer times:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "a4adba26-268c-41c2-b519-c40ec92d88ec",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "travel_time_matrix_computer = TravelTimeMatrixComputer(\n",
+    "    transport_network,\n",
+    "    origins=origin,\n",
+    "    destinations=points,\n",
+    "    departure=datetime.datetime(2022,2,22,8,30),\n",
+    "    transport_modes=[TransitMode.TRANSIT, LegMode.WALK],\n",
+    "    breakdown=True,\n",
+    ")\n",
+    "travel_time_matrix_detailed = travel_time_matrix_computer.compute_travel_times()\n",
+    "travel_time_matrix_detailed.head()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "782b8531-4437-40ca-8fc7-c0941edbb163",
+   "metadata": {},
+   "source": [
+    "As you can see, the result contains much more information than earlier, see the following table for explanations:\n",
+    "\n",
+    "| Column        | Description                                                          | Data type |\n",
+    "| ------------- | -------------------------------------------------------------------- | --------- |\n",
+    "| **routes**        | The route-ids (lines) used during the trip                           | list      |\n",
+    "| **board_stops**   | The stop-ids of the boarding stops                                   | list      |\n",
+    "| **alight_stops**  | The stop-ids of the alighting stops                                  | list      |\n",
+    "| **ride_times**    | In vehicle ride times of individual journey legs                     | list      |\n",
+    "| **access_time**   | The time it takes for the \"first mile\" of a trip                     | float     |\n",
+    "| **egress_time**   | The time it takes for the \"last mile\" of a trip                      | float     |\n",
+    "| **transfer_time** | The time it takes to transfer from vechile to another                | float     |\n",
+    "| **wait_times**    | The time(s) it take to wait for the vehicle at a stop                | list      |\n",
+    "| **total_time**    | Sum(ride_times, access_time, egress_time, transfer_time, wait_times) | float     |\n",
+    "| **n_iterations**  | Number of iterations used for calculating the travel times           | int       |\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "bfa6c86e-33e6-41ab-ac93-3bc5420fd12d",
+   "metadata": {},
+   "source": [
+    "### Compute travel times for different percentiles\n",
+    "\n",
+    "Because `r5py` calculates travel times for all possible transit departure possibilities within an hour (with one minute frequency), we basically get a distribution of travel times. It is possible to gather and return information about the travel times at different percentiles of this distribution based on all computed trips (sorted from the fastest to slowest connections). By default, the returned time in `r5py` is the median travel time (i.e. `50`). You can access these percentiles by using a parameter `percentiles` which accepts a list of integers representing different percentiles, such as `[25, 50, 75]` which returns the travel times at those percentiles:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "7cf036ed-9080-448a-b091-872892e50143",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "travel_time_matrix_computer = TravelTimeMatrixComputer(\n",
+    "    transport_network,\n",
+    "    origins=origin,\n",
+    "    destinations=points,\n",
+    "    departure=datetime.datetime(2022,2,22,8,30),\n",
+    "    transport_modes=[TransitMode.TRANSIT, LegMode.WALK],\n",
+    "    percentiles=[25, 50, 75],\n",
+    ")\n",
+    "travel_time_matrix_detailed = travel_time_matrix_computer.compute_travel_times()\n",
+    "travel_time_matrix_detailed.head()"
    ]
   }
  ],
@@ -205,7 +576,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.10.4"
+   "version": "3.10.6"
   }
  },
  "nbformat": 4,
diff --git a/docs/requirements.txt b/docs/requirements.txt
index 53e16b8b..7e95c6f2 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -13,7 +13,6 @@ joblib
 jpype1
 jupyter-book>=0.7.0b
 mapclassify
-matplotlib
 myst-nb
 myst-parser
 nbsphinx


Test patch:
diff --git a/tests/test_travel_time_matrix_computer.py b/tests/test_travel_time_matrix_computer.py
index 22395ac7..4375a5d5 100644
--- a/tests/test_travel_time_matrix_computer.py
+++ b/tests/test_travel_time_matrix_computer.py
@@ -170,7 +170,8 @@ def test_all_to_all(self, transport_network_from_test_files, population_points):
         assert travel_time_matrix["from_id"].min() == travel_time_matrix["to_id"].min()
         assert travel_time_matrix["from_id"].max() == travel_time_matrix["to_id"].max()
         assert travel_time_matrix["travel_time"].min() >= 0
-        assert travel_time_matrix["travel_time"].max() in [50, 51]
+        # There can be a bit of fluctuation in the maximum travel time
+        assert travel_time_matrix["travel_time"].max() in [49, 50, 51, 52]
 
     def test_one_to_all(
         self, transport_network_from_test_files, population_points, origin_point
@@ -188,7 +189,7 @@ def test_one_to_all(
         assert travel_time_matrix["to_id"].min() == 0
         assert travel_time_matrix["to_id"].max() == 91
         # There can be a bit of fluctuation in the maximum travel time
-        assert travel_time_matrix["travel_time"].max() in [29, 30]
+        assert travel_time_matrix["travel_time"].max() in [28, 29, 30, 31]
 
     def test_one_to_all_with_breakdown(
         self,