Spatial interaction is a broad term encompassing the flow or movement over space which results from a decision process (O’Kelly 2009).
Distribution models are models that estimate the number of trips that occur between each origin zone and each destination zone. Distribution models start from assumptions about group trip making behaviour and the way this is influenced by external factors such as total trip ends and travel costs. The best known of these models is the gravity model. Gravity models are the most widely used types of interaction models Haynes and Fotheringham (2020), and were originally generated from an analogy with Newton’s gravitational law. These estimate trips for each cell in the matrix without directly using the observed trip pattern; therefore they are sometimes called synthetic as opposed to growth factor models Ortúzar and Willumsen (2011).
The first use of a gravity model within a framework of urban mobility was made by Casey Jr (1955), who applied this approach to simulate shopping trips made between the regional towns. The initial formulation of the model was as follows Ortúzar and Willumsen (2011):
\(T_{ij}\) are the journeys between origin \(i\) and destination \(j\)
\(P_{i}\) and \(P_{j}\) are the populations of the origin and destination cities for the trips
\(d_{ij}\) is the distance between both zones
\(\alpha\) is a proportionality factor, which allows the units of population to be transformed into trips
This formulation was later thought to be an analogy with the law of gravity too simple and some fundamental modifications were made to the basic model in Equation 1. The overall generated and attracted trips (\(O_i\) and \(D_j\)) were used instead of the populations of the zones and the effect of distance (\(d_{ij}\)) was modelled more efficiently using a decreasing function according to the distance or cost. After making these corrections, the resulting model was as follows:
\[
T_{ij} = \alpha O_iD_jf(c_{ij})
\tag{2}\]
where \(f(c_{ij})\) is the generalized function of journey cost, which represents the disincentive to travel as distance or cost increases.
If the trips are represented in a matrix as below, then in the context of the gravity models different types of models can be defined according to the total outflow or inflow totals. This produces a “family” of spatial interaction models (A. G. Wilson 1971): an origin-constrained model, a destination-constrained model, and a doubly constrained model.
From the trip matrix, two of the possible constraints to consider are:
\[
\sum_j T_{ij} = O_i
\tag{3}\]
\[
\sum_i T_{ij} = D_j
\tag{4}\]
These constraints show us that the sum of the trips on each of the matrix rows should be equal to the total number of trips generated (\(O_i\)) by the origin zone that the row refers to; analogously, the sum of the trips for each matrix column should correspond to the number of trips attracted (\(D_j\)) by the destination zone the column refers to. Ensuring that both constraints are met requires one to replace the proportionality factor \(\alpha\) by the two balancing factors\(A_i\) and \(B_j\). Making these substitutions yields the following expression:
\[
T_{ij} = A_i O_i B_j D_jf(c_{ij})
\tag{5}\]
This represents the classic version of the doubly constrained gravity model. The singly constrained versions of the model, both in origins and in destinations, can be derived by making one set of balancing factors \(A_i\) or \(B_i\) equal to 1. For instance, in an origin-constrained model, \(B_j = 1\) for all \(j\) and
\[
A_i = \frac{1}{\sum_j D_j f(c_{ij})}
\]
\(A_i\) imposes the constraint given in Equation 3, that is, the predicted total interaction volume leaving each origin should equal the known value, \(O_i\). The origin constrained model thus adopts the form:
\[
T_{ij} = A_i O_i D_jf(c_{ij})
\tag{6}\]
The same reasoning and mathematical treatment can be translated to the destination constrained models yielding:
\[
T_{ij} = O_i B_j D_jf(c_{ij})
\tag{7}\]
with
\[
B_j = \frac{1}{\sum_i O_i f(c_{ij})}
\]
In the case of a doubly constrained model, the balancing factors are
\[
A_i = \frac{1}{\sum_j D_j f(c_{ij})}
\tag{8}\]
and
\[
B_j = \frac{1}{\sum_i A_j f(c_{ij})}
\tag{9}\]
These balancing factors are interdependent, which is why in the case of the doubly constrained model the values of both groups are required. In practice, \(A_i\) and \(B_j\) are estimated by performing an iterative process initiated by making all \(B_j = 1\), then by considering \(f(c_{ij})\) and calculating \(A_i\). Following that, the \(B_j\) are once again calculated with these values and the steps are repeated until the process converges at a solution or it gets close enough to a defined stop criterion, which fits the observed data sufficiently well to maintain the trustworthiness of the model (Cordera et al. 2017). This will be illustrated in Section 0.0.4.
0.0.1 Interaction models considering transport mode.
Among the doubly constrained models, (A. Wilson 1970) proposed considering a combined form of trip distribution and modal choice. This Notebook explores Wilson’s concept to simulate the number of journeys made from origin zones \(i\) to the destination zones \(j\) by the transport mode \(m\) using the following 3 equations, see Equation 10
\(T_{ij}^m\) is the number of trips from origin \(i\) to destination \(j\) using mode \(m\)
\(O_i^m\) is the total number of trips generated at origin zone \(i\) by mode \(m\)
\(O_i\) is the total number of trips generated at origin zone \(i\)
\(D_j^m\) is the total number of trips attracted at destination \(j\) by mode \(m\)
\(D_j\) is the total number of trips attracted at destination \(j\)
\(d_{ij}^m\) is the cost (distance) of making a trip from origin \(i\) to destination \(j\) using mode \(m\)
First Equation of the SIMs
Let’s begin with the first equation. The above model can be re-specified as a Poisson regression as shown in Equation 11. This allows us to obtain among other things the coefficient \(\beta_m\) which estimates the effect of travel cost for each mode of transport
\(\mu_i\) and \(\alpha_j\) are the equivalent of the vectors of balancing factors \(A_i\) and \(B_j\), but in regression / log-linear modelling terminology can also be described as either dummy variables or fixed effects. They allow us to to account for the geographical effects of an area; in this case, the total flows emanating from and destining to a given commune.
0.0.2 Data preparation
In this section, we get our data into a suitable form for modeling i.e aggregating flows into \(OD\), \(O_i^m\), \(D_j^m\) etc
# A tibble: 5 x 8
OD orig_code Oi dest_code Dj vehic network_dist flows
<chr> <chr> <int> <chr> <int> <chr> <dbl> <int>
1 2923-2923 2923 43 2923 33 walk 0.230 33
2 2924-2924 2924 33 2924 50 walk 0.661 33
3 3258-3258 3258 31 3258 22 walk 0.457 22
4 2846-2846 2846 19 2846 22 walk 0.666 19
5 3252-3252 3252 22 3252 21 walk 1.07 18
0.0.3 Model flows
In this section, we model flows as in Equation 11 and obtain \(\beta\) for each mode:
Code
set.seed(2056)library(tidymodels)library(poissonreg)metrics <-metric_set(rmse, rsq)# Create a Poisson Model specificationps_reg <-poisson_reg() %>%set_engine("glm") %>%set_mode("regression")# Create a recipeps_reg_rec <-recipe(flows ~ orig_code + dest_code + network_dist,data = model_tflows_moto) %>%# Log coststep_log(network_dist) %>%step_dummy(all_nominal_predictors())# Combine model specification and recipe into a workflowps_reg_wf <-workflow() %>%add_recipe(ps_reg_rec) %>%add_model(ps_reg)# Functions that fits a model to the data and binds flow predictionsfit_predict <-function(tbl){# Fit model mdl = ps_reg_wf %>%fit(tbl)# Make predictions tbl <- mdl %>%augment(tbl) %>%mutate(.pred =round(.pred)) %>%relocate(.pred, .after = flows) %>%mutate(beta = mdl %>%tidy() %>%filter(str_detect(term, "dist")) %>%pull(estimate))return(tbl)}# Fit model to motobike datamodel_tflows_moto <-fit_predict(model_tflows_moto)# Fit model to car datamodel_tflows_car <-fit_predict(model_tflows_car)# Fit model to bike datamodel_tflows_bike <-fit_predict(model_tflows_bike)# Fit model to ebike datamodel_tflows_ebike <-fit_predict(model_tflows_ebike)# Fit model to taxi datamodel_tflows_taxi <-fit_predict(model_tflows_taxi)# Fit model to walk datamodel_tflows_walk <-fit_predict(model_tflows_walk)# Fit model to bus datamodel_tflows_bus <-fit_predict(model_tflows_bus)# Bind results into one tibbletm_flows <- model_tflows_moto %>%bind_rows(model_tflows_car) %>%bind_rows(model_tflows_bus) %>%bind_rows(model_tflows_bike) %>%bind_rows(model_tflows_ebike) %>%bind_rows(model_tflows_taxi) %>%bind_rows(model_tflows_walk)# Sample resultstm_flows %>%group_by(vehic) %>%slice_max(n =1, .pred, with_ties =FALSE)
# Model coefficietstm_flows %>%distinct(vehic, beta)
# A tibble: 7 x 2
vehic beta
<chr> <dbl>
1 moto -6.66e- 2
2 car -9.63e- 2
3 bus 1.58e-16
4 bike 2.57e- 1
5 ebike -2.53e- 1
6 taxi -6.06e- 3
7 walk -1.98e+ 0
Code
# Overall performance metricsmetrics(tm_flows, truth = flows, estimate = .pred)
# A tibble: 2 x 3
.metric .estimator .estimate
<chr> <chr> <dbl>
1 rmse standard 9.29
2 rsq standard 0.661
Our previous performance metrics were at:
rmse: 11.7798 and rsq: 0.3621 so a good improvement in reproducing the original trip matrices.
The bulk of the representational and policy relevance advantages of the gravity model lies in the deterrence function. Now that we have relatively usable deterrence factors \(\beta\) for each mode that sufficiently reproduce the observed trips, we can go ahead and illustrate how to calibrate the balancing factors \(A_i\) and \(B_j\):
0.0.4 Calibrating balancing factors
The balancing factors in Equation 8 and Equation 9 are interdependent, this means that the calculation of one set requires the values of another set. This suggests an iterative process similar to Furness (1965) which works well in practice.
Given a set of values for the deterrence function \(f(c_{ij})\):
set all \(B_j = 1.0\) and solve \(A_i\). In this context, solve for\(A_i\) means finding the correction factors that satisfy the trip generation constraints
with the latest \(A_i\) solve for \(B_j\), i.e correction factors that satisfy trip attraction constraints
keeping the \(B_j\)’s fixed, solve for \(A_i\) and repeat steps (2) and (3) until the changes are sufficiently small.
Code
# Function to calculate balancing factorsget_bf <-function(tbl, desired_convergence =0.001, verbose =FALSE){# Determine type of deterrence function network_dist = ps_reg_rec$template %>% dplyr::slice(1) %>%pull(network_dist) prep_network_dist = ps_reg_rec %>%prep() %>%juice() %>% dplyr::slice(1) %>%pull(network_dist)if(prep_network_dist ==log(network_dist)){ deterrence_function ="power_function" } else{ deterrence_function ="exponential_function" }# Set starting values as per algorithm tbl <- tbl %>%mutate(Ai =1,Bj =1,oldAi =5,oldBj =5,diff =abs((oldAi - Ai) / oldAi) )# Create convergence value and count convergence =1 count =0while (convergence > desired_convergence) {# Increment count count = count +1if(verbose){print(paste0("iteration ", count)) }# Calibrate Aiif(deterrence_function =="power_function"){ tbl = tbl %>%mutate(Ai = Bj * Dj * (network_dist^beta)) %>%group_by(orig_code) %>%mutate(Ai =1/sum(Ai)) %>%ungroup() } else { tbl = tbl %>%mutate(Ai = Bj * Dj *exp(network_dist*beta)) %>%group_by(orig_code) %>%mutate(Ai =1/sum(Ai))%>%ungroup() }# Now, if not the first iteration, calculate the# difference between the new Ai values and the old# Ai values and once done, overwrite the old Ai# values with the new ones.if(count ==1){ tbl <- tbl %>%mutate(oldAi = Ai) } else { tbl <- tbl %>%mutate(diff =abs((oldAi - Ai) / oldAi),oldAi = Ai) }# Optionally print out values cnvg_ai = tbl %>%pull(diff) %>%sum()if(verbose){print(paste("Ai convergence", cnvg_ai)) }# Now similar calibration for Bjif(deterrence_function =="power_function"){ tbl = tbl %>%mutate(Bj = Ai * Oi * (network_dist^beta)) %>%group_by(dest_code) %>%mutate(Bj =1/sum(Bj))%>%ungroup() } else { tbl = tbl %>%mutate(Bj = Ai * Oi *exp(network_dist*beta)) %>%group_by(dest_code) %>%mutate(Bj =1/sum(Bj))%>%ungroup() }# Now, if not the first iteration, calculate the# difference between the new Bj values and the old# Bj values and once done, overwrite the old Bj# values with the new ones.if(count ==1){ tbl <- tbl %>%mutate(oldBj = Bj) } else { tbl <- tbl %>%mutate(diff =abs((oldBj - Bj) / oldBj),oldBj = Bj) }# Optionally print out values convergence = tbl %>%pull(diff) %>%sum()if(verbose){print(paste("Bj convergence", convergence)) } } tbl <- tbl %>%select(-contains("old"), -diff)return(tbl)}
Let’s verify whether the above works by
calibrating the balancing factors
plugging them into the multiplicative formula \(T_{ij} = A_i^m O_i^m B_j^m D_jf(c_{ij^m})\) and predicting flows
comparing predicted flows with what we had predicted earlier (.pred) using Poisson regression
As stated earlier, these type of models can estimate trips for each cell in the matrix without directly using the observed trip pattern, provided future values for parameters like \(O_i\) and \(D_j\) are available or estimable. So let’s see how we would predict flows in case of a motorbike ban.
We’ll use survey response to identify motorbike users alternative vehicles and how this would change the total number of modal flows generated/attracted at different communes.
Note
Train option as an alternative means will be converted to bus since the train flows were too few to be modelled.
Code
library(tidytext)set.seed(2056)# Identify OD communes that motorbike users were previously usingOD_moto <- model_tflows_moto %>%distinct(OD) %>%pull(OD)# Since respondents provide alternative means, just randomly pick 1scenario_moto <- df_trips_flows %>%mutate(across(contains("_code"), as.character)) %>%filter(vehic =="moto") %>%filter(OD %in% OD_moto) %>%unnest_tokens(output = scenario_vehic, input = alt_veh, token ="words", drop = F) %>%relocate(scenario_vehic, .after = alt_veh) %>%group_by(id) %>%slice_sample(n =1) %>%ungroup() %>%group_by(orig_code, dest_code, OD, scenario_vehic) %>%summarize(flows =n(), network_dist =mean(network_dist)) %>%ungroup() %>%# Make train busmutate(scenario_vehic =case_when( scenario_vehic =="lighttrain"~"bus",TRUE~ scenario_vehic ))# Display summary of what motorbike users result toscenario_moto %>%group_by(scenario_vehic) %>%summarise(flows =sum(flows)) %>%arrange(-flows)
# A tibble: 6 x 2
scenario_vehic flows
<chr> <int>
1 bus 3016
2 car 2233
3 taxi 1308
4 ebike 1256
5 bike 1206
6 walk 849
The tibble above shows an example of a scenario and what motorbike respondents would result to. Using this info, let’s update our \(O_i^m\) s and \(D_j^m\)s and estimate flows.