OD Jittering

Author

R Felix

Note

This page is only relevant when using travel survey data.

When having OD pairs with a macro-scale zoning, we can jitter these desire lines by randomly spread the origins and destinations points at a given area, making the trips (in particular active modes) more realistic (Lovelace, Félix, and Carlino 2022).

For that, zones, OD pairs, as well as the trip volume are needed.

Jittered desire lines

With od package

See the example bellow, using OD tirps between districts in Lisbon, with the od package (Lovelace and Morgan 2024).

# Load packages
library(sf)
library(tidyverse)
library(mapview)
# install.packages("od")
library(od)
# Load data

# road network
road_network = st_read("data/Lisbon/Lisbon_road_network.gpkg")

# polygons
lisbon_zones = st_read("https://github.com/U-Shift/Traffic-Simulation-Models/releases/download/2025/Freguesias_Lx.gpkg")

# OD data
od_lisbon = readRDS(url("https://github.com/U-Shift/Traffic-Simulation-Models/releases/download/2025/ODtrips_Freguesias_Lx.rds"))
od_lisbon = od_lisbon |> 
  filter(Bike > 0) # keep only with more than 0 bike trips
head(od_lisbon)
  DICOFREor11 DICOFREde11    Car  CarP  Bike   Walk  Other  Total    Active
1      110601      110601 143.98 43.67 12.76 622.82 116.68 939.91 67.621368
2      110601      110602 136.85 36.10  2.74 403.72 231.32 810.73 50.135063
3      110601      110607  21.64 18.90  0.00   0.00  23.75  64.29  0.000000
4      110601      110608 133.25  5.44  0.00   8.65 377.05 524.39  1.649536
5      110601      110610  48.55  0.00  0.00   0.00  63.55 112.10  0.000000
6      110601      110611  31.18 22.83  0.00   0.00   7.28  61.29  0.000000
# create desire lines between centroids
od_lisbon_dl = od::od_to_sf(od_lisbon, lisbon_zones)

mapview(od_lisbon_dl, zcol = "Bike")

od_lisbon_jit = od::od_jitter(
  od = od_lisbon,
  z = lisbon_zones,
  
  population_column = 8, #total trips
  disag = FALSE
)
od_lisbon_jit_disag = od::od_jitter(
  od = od_lisbon,
  z = lisbon_zones,
  population_column = 8, #total trips
  disag = TRUE,
  max_per_od = 200 # max trips per line
)

mapview::mapview(od_lisbon_jit, lwd = 0.2)
mapview::mapview(od_lisbon_jit_disag, lwd = 0.2)

jittering

jittering with disaggregation

See the od::od_jitter() function for more options.

With odjitter package

The same but with Rust, wich is faster:

# install odjitter in rust
git clone https://github.com/dabreegster/odjitter && cd odjitter && cargo build --release && cp ./target/release/odjitter /usr/local/bin/
# install odjitter in R
remotes::install_github("dabreegster/odjitter", subdir = "r")
# Load packages
library(odjitter)
# Jitter with disagregation threshold of 200 trips
od_lisbon_jittered = odjitter::jitter(  #jitter
  od = od_lisbon,
  zones = lisbon_zones,
  subpoints = road_network, # road network verices. we can choose buildings, or so 
  disaggregation_key = "Total",
  disaggregation_threshold = 200
)

mapview::mapview(od_lisbon_jittered, lwd = 0.2)

Jittered origins and destinations

From the jittered desire lines to the points of origin and destination.

library(stplanr)

# add an id to the jittered pairs, so we can join later
od_lisbon_jittered_id = od_lisbon_jittered
od_lisbon_jittered_id$id = 1:nrow(od_lisbon_jittered_id)

#with stplanr
od_lisbon_jittered_points = line2df(od_lisbon_jittered)
od_lisbon_jittered_points_OR = od_lisbon_jittered_points |>
  select(L1, fx, fy) |> # from
  rename(id = L1,
         lon = fx,
         lat = fy)
od_lisbon_jittered_points_DE = od_lisbon_jittered_points |>
  select(L1, tx, ty) |> # to
  rename(id = L1,
         lon = tx,
         lat = ty)

# as sf
od_lisbon_jittered_points_OR_geo = st_as_sf(od_lisbon_jittered_points_OR,
                                            coords = c("lon", "lat"),
                                            crs = 4326)
od_lisbon_jittered_points_DE_geo = st_as_sf(od_lisbon_jittered_points_DE,
                                            coords = c("lon", "lat"),
                                            crs = 4326)

mapview(od_lisbon_jittered_points_OR_geo, col.regions = "red") + 
  mapview(od_lisbon_jittered_points_DE_geo, col.regions = "blue")

After routing with r5r, you may want to add the original O and D codes.

od_lisbon_jittered_r5r = od_lisbon_jittered_r5r |>
  mutate(id = as.integer(from_id)) |>
  select(id, total_duration, total_distance, route) |>
  left_join(od_lisbon_jittered_id |>
              st_drop_geometry(), # drop geom for left_join
            by="id")

# get geometry back
od_lisbon_jittered_r5r = st_as_sf(as.data.frame(st_drop_geometry(od_lisbon_jittered_r5r)),
                                  geometry = od_lisbon_jittered_r5r$geometry)

References

Lovelace, Robin, Rosa Félix, and Dustin Carlino. 2022. “Jittering: A Computationally Efficient Method for Generating Realistic Route Networks from Origin-Destination Data.” Findings, April. https://doi.org/10.32866/001c.33873.
Lovelace, Robin, and Malcolm Morgan. 2024. Od: Manipulate and Map Origin-Destination Data. https://github.com/itsleeds/od.