| Title: | Boundary Discontinuity Designs on Networks |
|---|---|
| Description: | Estimation and inference for boundary discontinuity designs on street networks. Extends the location-based framework of 'rd2d' (Cattaneo, Titiunik and Yu, <doi:10.48550/arXiv.2505.07989>) from Euclidean coordinates to network-native coordinates (signed network distance to the boundary and offset along it) and user-defined network metrics (edge costs, travel time, road-class transition penalties, precomputed matrices). Provides side-specific local polynomial estimates of boundary average treatment effect curves with robust bias-corrected inference (Calonico, Cattaneo and Titiunik, 2014, <doi:10.3982/ECTA11757>), uniform confidence bands, weighted and largest aggregate effects, fuzzy designs, covariates, derivative targets, heteroskedasticity-robust, cluster and network Conley (1999, <doi:10.1016/S0304-4076(98)00084-0>) standard errors, a port of the 'rd2d' direct plug-in bandwidth selector, and a network-normalised version of the local polynomial density test of Cattaneo, Jansson and Ma (2020, <doi:10.1080/01621459.2019.1635480>). |
| Authors: | Zach Winship [aut, cre, cph], Matias D. Cattaneo [ctb, cph] (author of rd2d and rddensity, from which the bandwidth selector and density test are ported), Rocío Titiunik [ctb, cph] (author of rd2d, from which the bandwidth selector is ported), Ruiqi Rae Yu [ctb, cph] (author of rd2d, from which the bandwidth selector is ported), Michael Jansson [ctb, cph] (author of rddensity, from which the density test is ported), Xinwei Ma [ctb, cph] (author of rddensity, from which the density test is ported) |
| Maintainer: | Zach Winship <[email protected]> |
| License: | GPL-3 |
| Version: | 0.1.0 |
| Built: | 2026-10-28 19:57:09 UTC |
| Source: | https://github.com/cran/rd2dnet |
A boundary line, the side that is treated, and optional masked stretches.
boundary( line, treated_region = NULL, treated_side = NULL, mask = NULL, tol = 1e-06 )boundary( line, treated_region = NULL, treated_side = NULL, mask = NULL, tol = 1e-06 )
line |
The boundary: an |
treated_region |
Polygon containing the treated side: a ring matrix, a
list of ring matrices (holes and several parts use the even-odd rule), or
an sf polygon. Points inside it, or within |
treated_side |
Alternatively |
mask |
A list of geometries (polygons as above, or sf geometries such as
a park) and/or numeric |
tol |
Distance at which a network node counts as lying on the boundary. |
An object of class rd2dnet_boundary.
boundary_project(), boundary_is_treated(), boundary_is_masked().
b <- boundary(rbind(c(0, 0), c(100, 0)), treated_side = "left", mask = list(rect_polygon(20, 0, 40, 10), c(80, 90))) b boundary_is_treated(b, rbind(c(50, 1), c(50, -1))) boundary_is_masked(b, c(10, 30, 50, 85))b <- boundary(rbind(c(0, 0), c(100, 0)), treated_side = "left", mask = list(rect_polygon(20, 0, 40, 10), c(80, 90))) b boundary_is_treated(b, rbind(c(50, 1), c(50, -1))) boundary_is_masked(b, c(10, 30, 50, 85))
Distances to the boundary line, arc-length positions along it, and points at given positions.
boundary_distance(b, xy) boundary_project(b, xy) boundary_interpolate(b, arc)boundary_distance(b, xy) boundary_project(b, xy) boundary_interpolate(b, arc)
b |
A |
xy |
Points: an |
arc |
Arc-length positions along the boundary line. |
boundary_distance(): Euclidean distance from each point to the
line. boundary_project(): arc-length position of each point's
projection onto the line. boundary_interpolate(): an n x 2 matrix of
coordinates.
b <- boundary(rbind(c(0, 0), c(100, 0)), treated_side = "left") boundary_distance(b, rbind(c(10, 5), c(120, 0))) boundary_project(b, rbind(c(10, 5), c(120, 0))) boundary_interpolate(b, c(0, 50))b <- boundary(rbind(c(0, 0), c(100, 0)), treated_side = "left") boundary_distance(b, rbind(c(10, 5), c(120, 0))) boundary_project(b, rbind(c(10, 5), c(120, 0))) boundary_interpolate(b, c(0, 50))
Equally spaced positions along the boundary
boundary_grid(b, n = NULL, spacing = NULL, start = 0, end = NULL)boundary_grid(b, n = NULL, spacing = NULL, start = 0, end = NULL)
b |
A |
n |
Number of positions (default 25) when |
spacing |
Distance between consecutive positions. |
start, end
|
Arc-length range (default: the whole line). |
Numeric vector of arc-length positions.
b <- boundary(rbind(c(0, 0), c(100, 0)), treated_side = "left") boundary_grid(b, n = 5) boundary_grid(b, spacing = 30)b <- boundary(rbind(c(0, 0), c(100, 0)), treated_side = "left") boundary_grid(b, n = 5) boundary_grid(b, spacing = 30)
Masked stretches of the boundary
boundary_is_masked(b, arc)boundary_is_masked(b, arc)
b |
A |
arc |
Arc-length positions along the boundary line. |
Logical: whether each arc-length position falls in a masked stretch.
b <- boundary(rbind(c(0, 0), c(100, 0)), treated_side = "left", mask = list(c(80, 90))) boundary_is_masked(b, c(10, 85))b <- boundary(rbind(c(0, 0), c(100, 0)), treated_side = "left", mask = list(c(80, 90))) boundary_is_masked(b, c(10, 85))
Treatment side of points
boundary_is_treated(b, xy)boundary_is_treated(b, xy)
b |
A |
xy |
Points: an |
Logical: whether each point lies on the treated side (inside
treated_region, within tol of it, or on treated_side of the line).
b <- boundary(rbind(c(0, 0), c(10, 0)), treated_side = "right") boundary_is_treated(b, rbind(c(5, 1), c(5, -1)))b <- boundary(rbind(c(0, 0), c(10, 0)), treated_side = "right") boundary_is_treated(b, rbind(c(5, 1), c(5, -1)))
Kernel distance and boundary offset at an evaluation point
coords_at(coords, j)coords_at(coords, j)
coords |
Output of |
j |
Evaluation point (1-based index into |
A list with d, the network distance from evaluation point j
to every observation (for kernel weights), and o, each observation's
signed offset along the boundary from j (NA when the boundary cannot
be reached).
city <- load_mock_midtown() coords <- network_coordinates(city$network(), city$boundary, city$obs, n_eval = 5) str(coords_at(coords, 3))city <- load_mock_midtown() coords <- network_coordinates(city$network(), city$boundary, city$obs, n_eval = 5) str(coords_at(coords, 3))
Tests for a discontinuity in the density of observations at the boundary
with the local polynomial density estimator of Cattaneo, Jansson and Ma
(as in rddensity), with jackknife standard errors and bias-corrected
inference, applied to the signed network distance s.
## S3 method for class 'rd2dnet_coords' density( x, eval_index = NULL, window = NULL, h = NULL, p = 2, q = NULL, normalize = "network", kernel = "triangular", weights = NULL, bwselect = "common", scaleregul = 1, ... )## S3 method for class 'rd2dnet_coords' density( x, eval_index = NULL, window = NULL, h = NULL, p = 2, q = NULL, normalize = "network", kernel = "triangular", weights = NULL, bwselect = "common", scaleregul = 1, ... )
x |
Output of |
eval_index, window
|
Restrict to observations (and streets) whose
boundary crossing lies within |
h |
Bandwidth, scalar or |
p, q
|
Local polynomial order of the CDF fit (the density is its slope)
and of the bias-corrected fit used for inference (default |
normalize |
|
kernel |
Kernel name or function (see |
weights |
Optional non-negative observation weights. |
bwselect |
|
scaleregul |
Weight on the variance of the bias estimate in the bandwidth rule. |
... |
Ignored. |
With the default normalize = "network", each observation's distance is
re-expressed as cumulative street length: G(a) is the total length of
street on that side lying within network distance a of the boundary, and
the test runs on sign * G(|s|). The density in that coordinate is
observations per metre of street, so jumps in street supply at the
boundary (a highway on one side, a dense grid on the other, a park
frontage) do not masquerade as sorting. Bandwidths are then in
street-length units. Masked stretches of the boundary are always excluded.
An object of class rdnet_density: the density jump diff
(f+ - f-, order p) and diff_bc (bias-corrected), its jackknife
standard error se, z, pvalue, the descriptive log ratio theta
(log(f+/f-)), side-wise densities f_left/f_right, bandwidths
h_left/h_right, sample sizes n_left/n_right and within the
bandwidth n_left_h/n_right_h, p, q and normalize.
Cattaneo, M. D., Jansson, M. and Ma, X. (2020). Simple Local Polynomial Density Estimators. Journal of the American Statistical Association 115(531): 1449-1455.
Cattaneo, M. D., Jansson, M. and Ma, X. (2018). Manipulation Testing Based on Density Discontinuity. Stata Journal 18(1): 234-261.
city <- load_mock_midtown() coords <- network_coordinates(city$network(), city$boundary, city$obs, n_eval = 5) density(coords) density(coords, normalize = "none")city <- load_mock_midtown() coords <- network_coordinates(city$network(), city$boundary, city$obs, n_eval = 5) density(coords) density(coords, normalize = "none")
One row per undirected street segment (the first edge of each node pair):
endpoint indices u and v, geometric length (the length column, or
the geometry's length), the metric cost of traversing it (NA for a
precomputed metric), and its geometry oriented from u to v.
edge_frame(net)edge_frame(net)
net |
A data frame with a list column geometry.
city <- load_mock_midtown() head(edge_frame(city$network())[, c("u", "v", "length", "cost")])city <- load_mock_midtown() head(edge_frame(city$network())[, c("u", "v", "length", "cost")])
Evaluation points of network coordinates
eval_table(coords)eval_table(coords)
coords |
Output of |
A data frame with one row per evaluation point: requested arc
position arc, nearest network node id node, arc position after
snapping onto the network node_arc, coordinates x, y, and masked.
city <- load_mock_midtown() coords <- network_coordinates(city$network(), city$boundary, city$obs, n_eval = 5) eval_table(coords)city <- load_mock_midtown() coords <- network_coordinates(city$network(), city$boundary, city$obs, n_eval = 5) eval_table(coords)
Kernel functions evaluated at u = d / h, with d a network distance.
get_kernel(kernel)get_kernel(kernel)
kernel |
|
A vectorised function of u.
k <- get_kernel("epanechnikov") k(c(0, 0.5, 1.5))k <- get_kernel("epanechnikov") k(c(0, 0.5, 1.5))
load_mock_midtown() loads the bundled synthetic Midtown-Manhattan-style
street grid and 3,000 simulated parcels (no internet needed); it is the
same data as the Python package's bundled city. make_mock_midtown()
simulates a new city of the same design with R's random number generator.
load_mock_midtown() make_mock_midtown(n_obs = 3000, seed = 0, park = TRUE)load_mock_midtown() make_mock_midtown(n_obs = 3000, seed = 0, park = TRUE)
n_obs |
Number of simulated parcels. |
seed |
Random seed (the global random number stream is left untouched). |
park |
Include the park (and mask its boundary frontage). |
Layout (metres): 11 north-south avenues 250 m apart ("primary"), 41
east-west streets 80 m apart numbered 40th-80th ("residential"), a
west-side highway ("motorway") joined to the grid by ramps
("motorway_link"), and a park north of the boundary between avenues 3
and 7 with no interior streets or parcels. The boundary is 60th Street
(y = 1600); the treated zone lies south of it. The outcome is
0.0008 (y - 1600) + 0.6 cos(x / 700) + 0.2 log(far) + tau(arc) T + N(0, 1)
with tau given by mock_true_tau().
A rd2dnet_mock_city list with nodes, edges, obs, the
boundary() boundary, the park polygon park, boundary_y,
true_tau (a function of arc length) and network(metric, ...), a
function returning the network_graph() with the given metric.
mock_midtown_obs, mock_midtown_nodes, mock_midtown_edges.
city <- load_mock_midtown() city head(city$obs) city$network("length") small <- make_mock_midtown(n_obs = 500, seed = 1)city <- load_mock_midtown() city head(city$obs) city$network("length") small <- make_mock_midtown(n_obs = 500, seed = 1)
A metric defines what distance means on a network_graph(). Distance is
dissimilarity: it does not have to be metres. The coordinates s and o,
the kernel distances and the bandwidths are all in the metric's units.
edge_cost(cost = "length", snap_scale = NULL, name = NULL) transition_cost( edge_cost = "length", class_attr = "highway", penalty = NULL, default_penalty = 0, connectors = character(), default_class = "unclassified", snap_scale = NULL ) precomputed(matrix, nodes = NULL) travel_time( speed_attr = "speed_kph", length_attr = "length", default_kph = NULL ) node_contrast(attr, weight, base = "length", snap_scale = NULL)edge_cost(cost = "length", snap_scale = NULL, name = NULL) transition_cost( edge_cost = "length", class_attr = "highway", penalty = NULL, default_penalty = 0, connectors = character(), default_class = "unclassified", snap_scale = NULL ) precomputed(matrix, nodes = NULL) travel_time( speed_attr = "speed_kph", length_attr = "length", default_kph = NULL ) node_contrast(attr, weight, base = "length", snap_scale = NULL)
cost |
Edge column name or a vectorised function (see Details). |
snap_scale |
Cost per unit of off-network snapping distance. Defaults
to 1 when the cost is |
name |
Label used when printing. |
edge_cost |
Base per-edge cost (column name or function), as in |
class_attr |
Edge column holding the edge class (OSM's |
penalty |
Transition penalties: a function |
default_penalty |
Penalty for unlisted pairs of different classes. |
connectors |
Classes that do not reset the remembered class (ramps, links, crosswalks). |
default_class |
Class of edges with a missing class value. |
matrix |
Square matrix; |
nodes |
Node ids for the rows and columns. Defaults to the network's node order. |
speed_attr, length_attr
|
Edge columns with the speed (km/h) and length (m). |
default_kph |
Speed used where |
attr |
Node column compared across each edge. |
weight |
Cost per unit of absolute difference in |
base |
Edge column with the base cost. |
edge_cost(): path cost is the sum of edge costs. cost is an edge
column name ("length", "travel_time") or a function. A function is
called once, vectorised over all edges, as cost(edges) or, if it takes
two arguments, cost(edges, nodes); edges has columns from and to
(node ids) plus every edge attribute, nodes has name, x, y and the
node attributes. It returns one non-negative cost per edge (Inf blocks
an edge).
transition_cost(): edge costs plus a penalty for changing edge class
(e.g. highway to local street). Edges whose class is in connectors
(ramps, links) are transparent: the penalty is charged between the last
non-connector class and the next one, so highway -> ramp -> highway stays
cheap while highway -> ramp -> residential is expensive.
precomputed(): a user-supplied node-by-node dissimilarity matrix.
Observations then snap to nodes, since per-edge costs are unknown.
travel_time(): seconds to traverse each edge from a length (m) and a
speed (km/h) column.
node_contrast(): base + weight * |attr(u) - attr(v)|, for land-use or
other node-level dissimilarity.
An object of class rd2dnet_metric.
edge_cost("length") edge_cost(function(edges) 2 * edges$length, name = "double length") travel_time() node_contrast("far", 50) transition_cost("length", class_attr = "highway", penalty = data.frame(from = "motorway", to = "residential", penalty = 2000), connectors = c("motorway_link", "trunk_link"))edge_cost("length") edge_cost(function(edges) 2 * edges$length, name = "double length") travel_time() node_contrast("far", 50) transition_cost("length", class_attr = "highway", penalty = data.frame(from = "motorway", to = "residential", penalty = 2000), connectors = c("motorway_link", "trunk_link"))
The bundled synthetic city used in examples, tests and the vignette (see
load_mock_midtown(), which assembles them with the boundary).
mock_midtown_obs: 3,000 simulated parcels with columns
projected coordinates (metres)
south of the boundary (60th Street, y < 1600)
floor-area ratio, used as intensity weight
city block id, for clustering
simulated outcome
mock_midtown_nodes: 450 street intersections with columns name, x,
y and a node-level floor-area ratio far.
mock_midtown_edges: street segments with columns from, to, length
(metres), highway (road class), speed_kph and travel_time (seconds).
Simulated; identical to the data bundled with the Python package
rd2dnet.
The treatment effect used to simulate the mock city's outcome, by arc
length along its boundary: 1.5 + 0.75 sin(2 pi x / 2500) with x the
easting.
mock_true_tau(arc)mock_true_tau(arc)
arc |
Arc-length positions along the mock boundary. |
Numeric vector of true effects.
mock_true_tau(c(0, 500, 1000))mock_true_tau(c(0, 500, 1000))
Translates point locations into network coordinates around a boundary:
the signed network distance s to the boundary (positive on the treated
side) and, at every evaluation point b along the boundary, the network
distance d(b) used for kernel weights and the signed offset o(b) along
the boundary. These replace rd2d's Euclidean (X1 - b1, X2 - b2).
network_coordinates( network, boundary, points, treated = NULL, eval_points = NULL, n_eval = 25, snap = "edge", lonlat = FALSE, split_edges = TRUE ) ## S3 method for class 'rd2dnet_coords' as.data.frame(x, row.names = NULL, optional = FALSE, j = NULL, ...)network_coordinates( network, boundary, points, treated = NULL, eval_points = NULL, n_eval = 25, snap = "edge", lonlat = FALSE, split_edges = TRUE ) ## S3 method for class 'rd2dnet_coords' as.data.frame(x, row.names = NULL, optional = FALSE, j = NULL, ...)
network |
A |
boundary |
A |
points |
Observation locations: an |
treated |
Optional treatment indicator; defaults to the side of the boundary each point is on. |
eval_points |
Arc-length positions along the boundary, or a two-column
matrix of coordinates to project onto it. Defaults to |
n_eval |
Number of default evaluation points. |
snap |
|
lonlat |
Points are longitude/latitude; project them into the
network's |
split_edges |
Insert nodes where edges cross the boundary (recommended). |
x |
Output of |
row.names, optional
|
Ignored (for compatibility with the generic). |
j |
Optional evaluation point index; adds the columns |
... |
Ignored. |
Each observation snaps to the street network on its own side of the
boundary; its off-network depth is charged at the network's snap_scale.
Repeated locations (panels, sensors) are processed once.
An object of class rd2dnet_coords, a list with (among others)
s, treated, xy, crossing_arc (where each observation reaches the
boundary), eval_arc, eval_node_arc, eval_xy, eval_masked, loc
(location id of each observation), n_obs, n_eval, n_locations, and
the (boundary-split) network and boundary. Use coords_at() for d
and o at an evaluation point, eval_table() for the evaluation points,
and as.data.frame() for a per-observation table.
city <- load_mock_midtown() coords <- network_coordinates(city$network(), city$boundary, city$obs, n_eval = 11) coords head(as.data.frame(coords, j = 5))city <- load_mock_midtown() coords <- network_coordinates(city$network(), city$boundary, city$obs, n_eval = 11) coords head(as.data.frame(coords, j = 5))
Metric distances between network nodes.
distances_from(net, sources, limit = Inf) nearest_source(net, sources) node_distance(net, a, b)distances_from(net, sources, limit = Inf) nearest_source(net, sources) node_distance(net, a, b)
net |
|
sources |
Node indices (row numbers of |
limit |
Distances beyond |
a, b
|
Node ids. |
distances_from(): a length(sources) x n_nodes matrix.
nearest_source(): a list with, for every node, the distance to the
nearest source (dist) and that source's node index (src, NA if
unreachable). node_distance(): a single distance.
nodes <- data.frame(name = 1:3, x = c(0, 1, 2), y = 0) net <- network_graph(data.frame(from = 1:2, to = 2:3, length = c(5, 7)), nodes) distances_from(net, 1) nearest_source(net, c(1, 3)) node_distance(net, 1, 3)nodes <- data.frame(name = 1:3, x = c(0, 1, 2), y = 0) net <- network_graph(data.frame(from = 1:2, to = 2:3, length = c(5, 7)), nodes) distances_from(net, 1) nearest_source(net, c(1, 3)) node_distance(net, 1, 3)
A street (or any spatial) network together with the metric that defines distance on it.
network_graph( edges, nodes, metric = "length", directed = FALSE, direction = "both", x = "x", y = "y", crs = NULL, snap_scale = NULL )network_graph( edges, nodes, metric = "length", directed = FALSE, direction = "both", x = "x", y = "y", crs = NULL, snap_scale = NULL )
edges |
Data frame with one row per edge: columns |
nodes |
Data frame with one row per node: a node id column ( |
metric |
How distance is measured: an edge column name ( |
directed |
Whether edges are one-way ( |
direction |
For directed networks: |
x, y
|
Names of the coordinate columns in |
crs |
Coordinate reference system label of the projected coordinates (used to project longitude/latitude points). |
snap_scale |
Metric cost per unit of off-network snapping distance. Defaults to the metric's own default (1 for length, 0 otherwise). |
An object of class rd2dnet_network.
network_graph_from_adjacency(), network_graph_from_igraph(),
network_graph_from_place(), network_coordinates().
nodes <- data.frame(name = c("a", "b", "c"), x = c(0, 100, 200), y = 0) edges <- data.frame(from = c("a", "b"), to = c("b", "c"), length = c(100, 100)) net <- network_graph(edges, nodes) net node_distance(net, "a", "c")nodes <- data.frame(name = c("a", "b", "c"), x = c(0, 100, 200), y = 0) edges <- data.frame(from = c("a", "b"), to = c("b", "c"), length = c(100, 100)) net <- network_graph(edges, nodes) net node_distance(net, "a", "c")
Each nonzero entry A[i, j] becomes an edge with weight (the entry) and
length (the straight-line distance between the two nodes).
network_graph_from_adjacency( adjacency, coords, nodes = NULL, directed = FALSE, metric = "weight", ... )network_graph_from_adjacency( adjacency, coords, nodes = NULL, directed = FALSE, metric = "weight", ... )
adjacency |
Square weighted adjacency matrix (base or |
coords |
|
nodes |
Optional node ids (default |
directed |
Whether entries are one-way. |
metric |
Metric specification, as in |
... |
Further arguments to |
A <- rbind(c(0, 2, 0), c(2, 0, 3), c(0, 3, 0)) net <- network_graph_from_adjacency(A, rbind(c(0, 0), c(1, 0), c(2, 0))) node_distance(net, 1, 3)A <- rbind(c(0, 2, 0), c(2, 0, 3), c(0, 3, 0)) net <- network_graph_from_adjacency(A, rbind(c(0, 0), c(1, 0), c(2, 0))) node_distance(net, 1, 3)
Vertices must carry projected coordinates; vertex names (if any) become node ids and edge attributes become edge columns.
network_graph_from_igraph(g, metric = "weight", x = "x", y = "y", ...)network_graph_from_igraph(g, metric = "weight", x = "x", y = "y", ...)
g |
An |
metric |
Metric specification, as in |
x, y
|
Vertex attributes holding the coordinates. |
... |
Further arguments to |
g <- igraph::make_ring(4) igraph::V(g)$x <- c(0, 1, 1, 0) igraph::V(g)$y <- c(0, 0, 1, 1) igraph::E(g)$weight <- 1 network_graph_from_igraph(g)g <- igraph::make_ring(4) igraph::V(g)$x <- c(0, 1, 1, 0) igraph::V(g)$y <- c(0, 0, 1, 1) igraph::E(g)$weight <- 1 network_graph_from_igraph(g)
Downloads streets with dodgr (which uses osmdata) and projects
them to the local UTM zone with sf. If a package is missing or the
download fails and offline_fallback is TRUE, the bundled mock Midtown
network is returned with a warning, so scripts keep working offline.
network_graph_from_place( query = NULL, bbox = NULL, network_type = "walk", metric = "length", offline_fallback = TRUE, dist = 1000, min_area = 250000, ... )network_graph_from_place( query = NULL, bbox = NULL, network_type = "walk", metric = "length", offline_fallback = TRUE, dist = 1000, min_area = 250000, ... )
query |
Place name, e.g. |
bbox |
Alternatively a bounding box |
network_type |
|
metric |
Metric specification, as in |
offline_fallback |
Return the bundled mock network when the download fails. |
dist, min_area
|
|
... |
Further arguments to |
A network_graph() with length (m), highway, and
travel_time (s) edge columns.
net <- suppressWarnings(network_graph_from_place("Midtown Manhattan, New York")) netnet <- suppressWarnings(network_graph_from_place("Midtown Manhattan, New York")) net
Network distances between observations
pairwise_distances(coords, idx, cols = NULL, cutoff = Inf)pairwise_distances(coords, idx, cols = NULL, cutoff = Inf)
coords |
Output of |
idx |
Observation indices for the rows. |
cols |
Observation indices for the columns (default |
cutoff |
Distances beyond |
A length(idx) x length(cols) matrix of network distances
(including both snapping depths); an observation is at distance 0 from itself.
city <- load_mock_midtown() coords <- network_coordinates(city$network(), city$boundary, city$obs, n_eval = 5) round(pairwise_distances(coords, 1:4), 1)city <- load_mock_midtown() coords <- network_coordinates(city$network(), city$boundary, city$obs, n_eval = 5) round(pairwise_distances(coords, 1:4), 1)
Project longitude/latitude to a planar coordinate system
project_lonlat(lon, lat, to_crs)project_lonlat(lon, lat, to_crs)
lon, lat
|
Longitude and latitude (WGS84). |
to_crs |
Target coordinate reference system (e.g. |
An n x 2 matrix of projected coordinates. Requires sf.
project_lonlat(-73.98, 40.76, "EPSG:32618")project_lonlat(-73.98, 40.76, "EPSG:32618")
Selects MSE- or coverage-error-optimal bandwidths (in metric units) at each
evaluation point. A port of rd2d's direct plug-in selector (rdbw2d) to
network coordinates: a rule-of-thumb pilot, a separate pilot bandwidth for
the bias, regularisation (scaleregul) and a higher-order bias term
(scalebiascrct).
rdbwnet( y, coords, p = 1, deriv = c(0, 0), tangvec = NULL, covs = NULL, kernel = "triangular", bwselect = "mserd", vce = "hc1", cluster = NULL, fuzzy = NULL, weights = NULL, bwcheck = NULL, scaleregul = 1, scalebiascrct = 1 )rdbwnet( y, coords, p = 1, deriv = c(0, 0), tangvec = NULL, covs = NULL, kernel = "triangular", bwselect = "mserd", vce = "hc1", cluster = NULL, fuzzy = NULL, weights = NULL, bwcheck = NULL, scaleregul = 1, scalebiascrct = 1 )
y |
Outcome, one value per observation in |
coords |
Output of |
p |
Polynomial order of the local fit. |
deriv, tangvec, covs, kernel, cluster, fuzzy, weights
|
As in |
bwselect |
|
vce |
|
bwcheck |
Every bandwidth is at least the distance to the
|
scaleregul, scalebiascrct
|
Weights on the bias-variance regularisation and the higher-order bias term. |
deriv/tangvec target a derivative jump and covs are partialled out
(with coefficients common to both sides, estimated at the pilot
bandwidth), as in rd2d. The cer* variants shrink the MSE-optimal
bandwidth by rd2d's coverage-error rate factor n^(1/(2p+4) - 1/(p+4)).
Fuzzy designs use the linearised outcome y / fs - itt / fs^2 * D, as rd2d does.
A data frame with one row per evaluation point: the
control/treated bandwidths h0/h1 (and h when they are equal), the
pilots dn, bn0, bn1, the MSE constants (V, B, Reg1, Reg2
per side, suffixed 0 for control and 1 for treated), the bounds
hmin0/hmin1/hmax0/hmax1, local sample sizes n0/n1, and a status.
Cattaneo, M. D., Titiunik, R. and Yu, R. R. (2026). rd2d: Causal Inference in Boundary Discontinuity Designs. arXiv:2505.07989.
city <- load_mock_midtown() coords <- network_coordinates(city$network(), city$boundary, city$obs, n_eval = 7) bw <- rdbwnet(city$obs$outcome, coords) bw[, c("arc", "h0", "h1", "n0", "n1", "status")]city <- load_mock_midtown() coords <- network_coordinates(city$network(), city$boundary, city$obs, n_eval = 7) bw <- rdbwnet(city$obs$outcome, coords) bw[, c("arc", "h0", "h1", "n0", "n1", "status")]
Estimates the boundary average treatment effect curve (BATEC) at each
evaluation point along the boundary, with robust bias-corrected inference
and a uniform confidence band. This carries rd2d's location-based
estimator over to network coordinates: (s, o(b)) replace
(X1 - b1, X2 - b2) and the network distance d(b) replaces the Euclidean
distance in the kernel.
rdnet( y, coords, h = NULL, p = 1, q = NULL, deriv = c(0, 0), tangvec = NULL, covs = NULL, kernel = "triangular", bwselect = "mserd", vce = "hc1", cluster = NULL, conley_cutoff = NULL, conley_distance = "network", conley_resid_scale = 3, conley_resid_order = NULL, fuzzy = NULL, weights = NULL, pooled = FALSE, level = 95, cbands = TRUE, repp = 2000, min_obs = NULL, scaleregul = 3, scalebiascrct = 1, seed = NULL )rdnet( y, coords, h = NULL, p = 1, q = NULL, deriv = c(0, 0), tangvec = NULL, covs = NULL, kernel = "triangular", bwselect = "mserd", vce = "hc1", cluster = NULL, conley_cutoff = NULL, conley_distance = "network", conley_resid_scale = 3, conley_resid_order = NULL, fuzzy = NULL, weights = NULL, pooled = FALSE, level = 95, cbands = TRUE, repp = 2000, min_obs = NULL, scaleregul = 3, scalebiascrct = 1, seed = NULL )
y |
Outcome, one value per observation in |
coords |
Output of |
h |
Bandwidth in metric units: a scalar, |
p, q
|
Polynomial order for estimation and for bias-corrected
inference (default |
deriv |
|
tangvec |
Direction |
covs |
Covariates (vector or |
kernel |
|
bwselect |
Bandwidth selector used when |
vce |
|
cluster |
Cluster labels, one per observation; implies |
conley_cutoff, conley_distance
|
Spatial HAC with Bartlett weights over
|
conley_resid_scale, conley_resid_order
|
Conley residuals come from an
auxiliary fit of order |
fuzzy |
Treatment take-up |
weights |
Non-negative intensity weights (e.g. floor area) multiplying the kernel weights. |
pooled |
Fit one regression |
level |
Confidence level in percent. |
cbands |
Add the uniform confidence band |
repp |
Gaussian draws used to simulate the uniform critical value. |
min_obs |
Minimum local observations per side (default |
scaleregul, scalebiascrct
|
Bandwidth-selection regularisation and
higher-order bias weights, passed to |
seed |
Seed for the critical-value simulation (reproducible bands and LBATE). The global random number stream is left untouched. |
An object of class rdnet, a list with
tableone row per evaluation point: eval, arc, node_arc,
bandwidths h0, h1 (and h when equal), local sample sizes n0,
n1, the order-p estimate tau and its standard error se, the
bias-corrected tau_bc with robust se_rb, the robust interval
ci_lo/ci_hi, pvalue, a status ("ok", "masked",
"insufficient", "singular", "no bandwidth", "weak first stage"), and optionally the uniform band cb_lo/cb_hi. Fuzzy
designs add itt, fs and fs_se.
covthe J x J covariance of tau_bc across evaluation
points (rd2d's params_cov), NA for points not estimated; its
diagonal is se_rb^2.
critthe uniform critical value implied by cov.
bandwidthsthe rdbwnet() output when h was selected.
plus p, q, kernel, vce, level, metric, fuzzy and coords.
Use wbate() and lbate() for aggregate effects, summary(), plot(),
coef(), vcov() and confint() for the usual summaries.
Cattaneo, M. D., Titiunik, R. and Yu, R. R. (2026). rd2d: Causal Inference in Boundary Discontinuity Designs. arXiv:2505.07989.
Calonico, S., Cattaneo, M. D. and Titiunik, R. (2014). Robust Nonparametric Confidence Intervals for Regression-Discontinuity Designs. Econometrica 82(6): 2295-2326.
Conley, T. G. (1999). GMM Estimation with Cross Sectional Dependence. Journal of Econometrics 92(1): 1-45.
city <- load_mock_midtown() coords <- network_coordinates(city$network(), city$boundary, city$obs, n_eval = 9) res <- rdnet(city$obs$outcome, coords, weights = city$obs$far, seed = 1) res ok <- res$table$status == "ok" wbate(res, as.numeric(ok)) plot(res)city <- load_mock_midtown() coords <- network_coordinates(city$network(), city$boundary, city$obs, n_eval = 9) res <- rdnet(city$obs$outcome, coords, weights = city$obs$far, seed = 1) res ok <- res$table$status == "ok" wbate(res, as.numeric(ok)) plot(res)
Summaries of rdnet results
## S3 method for class 'rdnet' summary(object, wbate = NULL, lbate = FALSE, ...) ## S3 method for class 'summary.rdnet' print(x, digits = 3, ...) ## S3 method for class 'rdnet' print(x, ...) ## S3 method for class 'rdnet' coef(object, type = c("tau_bc", "tau"), ...) ## S3 method for class 'rdnet' vcov(object, ...) ## S3 method for class 'rdnet' confint(object, parm, level = object$level/100, ...) ## S3 method for class 'rdnet' as.data.frame(x, row.names = NULL, optional = FALSE, ...) ## S3 method for class 'rdnet' plot(x, band = TRUE, true_tau = NULL, ...)## S3 method for class 'rdnet' summary(object, wbate = NULL, lbate = FALSE, ...) ## S3 method for class 'summary.rdnet' print(x, digits = 3, ...) ## S3 method for class 'rdnet' print(x, ...) ## S3 method for class 'rdnet' coef(object, type = c("tau_bc", "tau"), ...) ## S3 method for class 'rdnet' vcov(object, ...) ## S3 method for class 'rdnet' confint(object, parm, level = object$level/100, ...) ## S3 method for class 'rdnet' as.data.frame(x, row.names = NULL, optional = FALSE, ...) ## S3 method for class 'rdnet' plot(x, band = TRUE, true_tau = NULL, ...)
object, x
|
An |
wbate |
Optional WBATE weights (see |
lbate |
Add the LBATE row (see |
... |
Further arguments (passed to |
digits |
Digits to print. |
type |
For |
parm |
Evaluation points (indices) for |
level |
Confidence level for |
row.names, optional
|
Ignored. |
band |
Draw the uniform band. |
true_tau |
Optional true effects at the evaluation points, drawn dashed. |
summary() returns an object of class summary.rdnet; coef() the
estimates by evaluation point; vcov() the joint covariance x$cov;
confint() robust intervals tau_bc -/+ z se_rb; as.data.frame() the
result table; plot() draws the BATEC curve with pointwise robust
intervals and the uniform band and returns x invisibly.
A convenience constructor for an axis-aligned rectangle, usable wherever a polygon is accepted (treated regions, masks).
rect_polygon(xmin, ymin, xmax, ymax)rect_polygon(xmin, ymin, xmax, ymax)
xmin, ymin, xmax, ymax
|
Corners of the rectangle. |
A 5 x 2 matrix of ring vertices (closed).
rect_polygon(0, 0, 10, 5)rect_polygon(0, 0, 10, 5)
Each point reaches node u at cost cost_u and node v at cost cost_v
along its street segment, after an off-network depth (Euclidean units).
Points snapped to a node have u == v and zero edge costs.
snap_points(net, xy, allowed = NULL, mode = "edge")snap_points(net, xy, allowed = NULL, mode = "edge")
net |
|
xy |
Points: an |
allowed |
Optional logical vector over nodes. With |
mode |
|
A list with integer vectors u, v (node indices) and numeric
vectors cost_u, cost_v, depth.
nodes <- data.frame(name = 1:2, x = c(0, 100), y = 0) net <- network_graph(data.frame(from = 1, to = 2, length = 100), nodes) snap_points(net, rbind(c(30, 10)))nodes <- data.frame(name = 1:2, x = c(0, 100), y = 0) net <- network_graph(data.frame(from = 1, to = 2, length = 100), nodes) snap_points(net, rbind(c(30, 10)))
wbate() is the weighted boundary average treatment effect w' tau
(rd2d's WBATE): weights has one entry per evaluation point and is
normalised to sum to one; points that were not estimated must get weight
0. Its standard error sqrt(w' Sigma w) uses the joint covariance
x$cov, so it accounts for the correlation between nearby points.
wbate(x, weights) lbate(x)wbate(x, weights) lbate(x)
x |
An |
weights |
One weight per evaluation point. |
lbate() is the largest boundary average treatment effect
max_j tau(b_j) (rd2d's LBATE), with interval
[max_j (tau_bc - crit se_rb), max_j (tau_bc + crit se_rb)] over the
estimated points, crit the uniform critical value, so it inherits the
uniform band's joint coverage.
A named numeric vector: tau, tau_bc, se_rb, ci_lo,
ci_hi, pvalue (the bias-corrected estimate drives the interval and
p-value; se_rb and pvalue are NA for LBATE).
city <- load_mock_midtown() coords <- network_coordinates(city$network(), city$boundary, city$obs, n_eval = 9) res <- rdnet(city$obs$outcome, coords, h = 600, seed = 1) ok <- res$table$status == "ok" wbate(res, as.numeric(ok)) lbate(res)city <- load_mock_midtown() coords <- network_coordinates(city$network(), city$boundary, city$obs, n_eval = 9) res <- rdnet(city$obs$outcome, coords, h = 600, seed = 1) ok <- res$table$status == "ok" wbate(res, as.numeric(ok)) lbate(res)
Inserts a node wherever an edge crosses the boundary line, so that every
path between the two sides passes through a node on the boundary. Columns
listed in extensive are divided in proportion to the split; all others
are copied. network_coordinates() does this automatically.
with_boundary_crossings(net, b, extensive = c("length", "travel_time"))with_boundary_crossings(net, b, extensive = c("length", "travel_time"))
net |
|
b |
A |
extensive |
Edge columns that scale with length. |
A network (the same one when nothing needs splitting), with
attribute "n_crossings_inserted".
nodes <- data.frame(name = 1:2, x = 0, y = c(0, 100)) net <- network_graph(data.frame(from = 1, to = 2, length = 100), nodes) b <- boundary(rbind(c(-10, 40), c(10, 40)), treated_region = rect_polygon(-50, -50, 50, 40)) split <- with_boundary_crossings(net, b) split$edgesnodes <- data.frame(name = 1:2, x = 0, y = c(0, 100)) net <- network_graph(data.frame(from = 1, to = 2, length = 100), nodes) b <- boundary(rbind(c(-10, 40), c(10, 40)), treated_region = rect_polygon(-50, -50, 50, 40)) split <- with_boundary_crossings(net, b) split$edges