| Title: | Spatial-X (SLX) Models for Applied Researchers |
|---|---|
| Description: | Tools for estimating, interpreting, and visualizing Spatial-X (SLX) regression models. Provides a formula-based interface with first-class support for variable-specific weights matrices, higher-order spatial lags, temporally-lagged spatial variables (TSLS), and tidy effects decomposition (direct, indirect, total). Designed to lower the barrier to SLX modeling for applied researchers who already work with 'sf' and 'lm'-style formulas. Methods follow Wimpy, Whitten, and Williams (2021) <doi:10.1086/710089>. |
| Authors: | Cameron Wimpy [aut, cre] (ORCID: <https://orcid.org/0000-0002-2049-5229>) |
| Maintainer: | Cameron Wimpy <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.1.1 |
| Built: | 2026-05-27 06:23:56 UTC |
| Source: | https://github.com/cwimpy/slxr |
A 179-country cross-section of defense spending and conflict indicators for 1995, drawn from the replication archive of Wimpy, Whitten, and Williams (2021). Three row-standardized spatial weights matrices connect the units through different channels: contiguity, alliance, and defense pact.
defense_burdendefense_burden
A named list with four elements:
dataA tibble with 179 rows and 12 variables, arranged by
Correlates of War country code (ccode). Variables:
ccode - Correlates of War country code.
year - Calendar year (1995).
ch_milex - Change in military expenditures (outcome).
milex_tm1 - Lagged military expenditures.
log_pop_tm1 - Lagged log population.
civilwar_tm1 - Lagged civil war indicator.
total_wars_tm1 - Lagged count of interstate wars.
alliance_us - Alliance with the United States.
ch_milex_us - Change in U.S. military expenditures.
ch_milex_ussr - Change in Soviet/Russian military
expenditures.
region - Integer region code (1-5).
region_name - Factor: Europe, N Africa/Middle East,
Africa, Asia/Oceania, Americas.
W_contigA 179 x 179 row-standardized sparse
dgCMatrix encoding geographic contiguity.
W_allianceA 179 x 179 row-standardized sparse
dgCMatrix encoding alliance ties.
W_defenseA 179 x 179 row-standardized sparse
dgCMatrix encoding mutual defense pacts.
This cross-section is intended for pedagogical demonstration of
variable-specific SLX specifications. The original paper estimates a
pooled panel SLX across 1950-2008 with year-specific weights matrices;
that full-panel replication requires block-diagonal W support, which
is planned for slxr v0.2.
Wimpy, Whitten, and Williams (2021) replication archive, Journal of Politics Dataverse. doi:10.1086/710089
Wimpy, C., Whitten, G. D., & Williams, L. K. (2021). X Marks the Spot: Unlocking the Treasure of Spatial-X Models. Journal of Politics, 83(2), 722-739.
data(defense_burden) dim(defense_burden$data) dim(defense_burden$W_contig)data(defense_burden) dim(defense_burden$data) dim(defense_burden$W_contig)
The full country-year panel underlying Wimpy, Whitten, and Williams (2021) Table 3, Model 3. Includes a tibble of 7,661 country-year observations and three named lists of row-standardized sparse weights matrices, one matrix per year, encoding contiguity, alliance, and defense-pact connections.
defense_burden_paneldefense_burden_panel
A named list:
dataA tibble with 7,661 rows and the same 12 columns as
defense_burden$data, but spanning 1951-2008.
W_contigNamed list of 58 sparse matrices, one per year, keyed by year as a string. Each matrix is row-standardized and contains the countries observed in that year.
W_allianceAs above, for alliance ties.
W_defenseAs above, for mutual defense pacts.
Sample excludes observations with missing covariates. Panel is unbalanced: between 63 and 187 countries per year.
Wimpy, Whitten, and Williams (2021) replication archive, Journal of Politics Dataverse. doi:10.1086/710089
defense_burden for the 1995 cross-section.
data(defense_burden_panel) names(defense_burden_panel$W_contig)[1:5] dim(defense_burden_panel$W_contig[["1990"]])data(defense_burden_panel) names(defense_burden_panel$W_contig)[1:5] dim(defense_burden_panel$W_contig[["1990"]])
Estimates a Spatial-X regression of the form
where W is a spatial weights matrix and X includes both the
directly-entering regressors and a user-specified subset that is
spatially lagged. SLX estimation is OLS on the augmented design matrix,
which makes estimation, interpretation, and effects decomposition much
simpler than for SAR-family models (Wimpy, Whitten, and Williams 2021).
slx( formula, data, W = NULL, lag = NULL, order = 1L, spatial = NULL, time_lag = 0L, id = NULL, time = NULL, na.action = stats::na.omit )slx( formula, data, W = NULL, lag = NULL, order = 1L, spatial = NULL, time_lag = 0L, id = NULL, time = NULL, na.action = stats::na.omit )
formula |
A standard model formula, e.g. |
data |
A data frame (or |
W |
An |
lag |
Character vector of variable names from |
order |
Integer vector giving the orders of |
spatial |
Optional named list for variable-specific weights
matrices. Names must match variables in |
time_lag |
Integer, number of time periods to lag the spatial
terms (TSLS). Requires |
id, time
|
Column names identifying the panel unit and period.
Required for panel mode or TSLS. |
na.action |
How to handle missing values. Defaults to
|
An object of class slx with elements:
fitThe underlying lm object.
formulaThe expanded model formula.
callThe original call.
WThe weights matrix (or list thereof) used.
lag_termsA data frame mapping spatial-lag terms to their source variable, W matrix, order, and time lag.
dataThe (possibly augmented) model frame.
panelLogical flag.
When id and time are NULL, slx() treats the data as a single
cross-section: rows of data must correspond, in order, to the rows
and columns of W. When both id and time are supplied, slx()
enters panel mode and performs block-wise spatial lagging period by
period. In panel mode the weights matrices must have dimnames
matching the unit identifiers in data[[id]]; the same units need
not appear in every period (unbalanced panels are supported).
In panel mode, a W argument can be either (a) a single slx_W
object applied to every period or (b) a named list of slx_W
objects keyed by the stringified time value (e.g.
list("1990" = W_90, "1991" = W_91)). The same options apply to
any entry inside spatial.
The time_lag argument implements equation 7 of Wimpy, Whitten,
and Williams (2021): it lags every constructed spatial term W_t x
by time_lag periods within each unit so that the regressor becomes
W_t x_{t - time_lag}. The first time_lag periods per unit drop
out, mirroring any temporally-lagged variable.
Wimpy, C., Whitten, G. D., & Williams, L. K. (2021). X Marks the Spot: Unlocking the Treasure of Spatial-X Models. Journal of Politics, 83(2), 722-739.
Vega, S. H., & Elhorst, J. P. (2015). The SLX Model. Journal of Regional Science, 55(3), 339-363.
data(defense_burden) W_c <- slx_weights(style = "custom", matrix = defense_burden$W_contig, row_standardize = FALSE) fit <- slx(ch_milex ~ milex_tm1 + log_pop_tm1 + civilwar_tm1, data = defense_burden$data, W = W_c, lag = "civilwar_tm1") slx_effects(fit)data(defense_burden) W_c <- slx_weights(style = "custom", matrix = defense_burden$W_contig, row_standardize = FALSE) fit <- slx(ch_milex ~ milex_tm1 + log_pop_tm1 + civilwar_tm1, data = defense_burden$data, W = W_c, lag = "civilwar_tm1") slx_effects(fit)
Builds a tidy tibble of fit statistics (observations, coefficient
count, R-squared, adjusted R-squared, residual standard error, AIC,
BIC) for any combination of lm and slx objects. If a weights
matrix is supplied, also reports Moran's I on each model's
residuals - the standard diagnostic for spatial autocorrelation
left unexplained by the fitted model.
slx_compare(..., W = NULL)slx_compare(..., W = NULL)
... |
Named |
W |
Optional weights matrix to use for Moran's I on residuals.
Accepts an |
A tibble with one row per model.
data(defense_burden) W <- slx_weights(style = "custom", matrix = defense_burden$W_contig, row_standardize = FALSE) ols <- lm(ch_milex ~ milex_tm1 + civilwar_tm1, data = defense_burden$data) slx_fit <- slx(ch_milex ~ milex_tm1 + civilwar_tm1, data = defense_burden$data, W = W, lag = "civilwar_tm1") slx_compare(OLS = ols, SLX = slx_fit, W = W)data(defense_burden) W <- slx_weights(style = "custom", matrix = defense_burden$W_contig, row_standardize = FALSE) ols <- lm(ch_milex ~ milex_tm1 + civilwar_tm1, data = defense_burden$data) slx_fit <- slx(ch_milex ~ milex_tm1 + civilwar_tm1, data = defense_burden$data, W = W, lag = "civilwar_tm1") slx_compare(OLS = ols, SLX = slx_fit, W = W)
For an SLX model the effects decomposition is trivial — no matrix
inversion, no simulation. For each variable that enters both
directly and as a spatial lag, the direct effect is its OLS coefficient
and the indirect effect at order is
. Standard errors come straight from vcov().
slx_effects(object, by_order = FALSE, conf.level = 0.95)slx_effects(object, by_order = FALSE, conf.level = 0.95)
object |
An |
by_order |
Logical; if |
conf.level |
Confidence level for reported intervals. Default 0.95. |
A tibble with columns variable, w_name, order (when
by_order = TRUE), type (direct/indirect/total), estimate,
std.error, conf.low, conf.high, p.value.
data(defense_burden) W <- slx_weights(style = "custom", matrix = defense_burden$W_contig, row_standardize = FALSE) fit <- slx(ch_milex ~ milex_tm1 + civilwar_tm1, data = defense_burden$data, W = W, lag = "civilwar_tm1") slx_effects(fit)data(defense_burden) W <- slx_weights(style = "custom", matrix = defense_burden$W_contig, row_standardize = FALSE) fit <- slx(ch_milex ~ milex_tm1 + civilwar_tm1, data = defense_burden$data, W = W, lag = "civilwar_tm1") slx_effects(fit)
For SLX models fit with higher-order lags (order = 1:k), shows the
size of the indirect effect at each order, with confidence intervals.
slx_plot_decay(fit, variables = NULL, conf.level = 0.95)slx_plot_decay(fit, variables = NULL, conf.level = 0.95)
fit |
An |
variables |
Optional character vector restricting to specific lagged variables. Default plots all. |
conf.level |
Confidence level. |
A ggplot object.
Produces a ggplot of the direct, indirect, and total effects from an
SLX model, with 95% confidence intervals. For models with
variable-specific weights matrices, effects are faceted by w_name
so spillover patterns from each matrix are visible side-by-side.
slx_plot_effects( fit, types = c("direct", "indirect", "total"), conf.level = 0.95, by_order = FALSE, ... )slx_plot_effects( fit, types = c("direct", "indirect", "total"), conf.level = 0.95, by_order = FALSE, ... )
fit |
An |
types |
Character vector of effect types to include. Any subset of
|
conf.level |
Confidence level for the intervals. Default 0.95. |
by_order |
Logical; if |
... |
Passed to |
A ggplot object.
data(defense_burden) W <- slx_weights(style = "custom", matrix = defense_burden$W_contig, row_standardize = FALSE) fit <- slx(ch_milex ~ milex_tm1 + civilwar_tm1, data = defense_burden$data, W = W, lag = "civilwar_tm1") slx_plot_effects(fit)data(defense_burden) W <- slx_weights(style = "custom", matrix = defense_burden$W_contig, row_standardize = FALSE) fit <- slx(ch_milex ~ milex_tm1 + civilwar_tm1, data = defense_burden$data, W = W, lag = "civilwar_tm1") slx_plot_effects(fit)
Given a fitted SLX model, a choice of variable, and a target unit, returns a plot of the predicted change in the outcome across every unit in the sample under a unit shock to that variable in the target unit.
slx_plot_shock(fit, variable, unit, magnitude = 1, geom = NULL, top_n = 15)slx_plot_shock(fit, variable, unit, magnitude = 1, geom = NULL, top_n = 15)
fit |
A cross-sectional |
variable |
Character, the name of a spatially-lagged regressor
in |
unit |
Integer row index (or character id, if the weights
matrix has dimnames matching something in |
magnitude |
Numeric, shock size. Default |
geom |
Optional |
top_n |
For the non-map plot, how many non-zero indirect
effects to show. Default |
For an SLX model at first order with channels indexed by c, the
predicted change at unit j from a shock of size magnitude to
variable x in unit i is
Higher-order lags add additional theta_{c,k} (W_c^k)[j, i] terms.
No simulation is required: the shock effect is a single column of
the spatial multiplier.
If an sf object with matching row count is supplied via geom,
the result is drawn as a choropleth. Otherwise a horizontal bar of
the largest effects is returned.
A ggplot object.
data(defense_burden) W_c <- slx_weights(style = "custom", matrix = defense_burden$W_contig, row_standardize = FALSE) fit <- slx(ch_milex ~ milex_tm1 + civilwar_tm1, data = defense_burden$data, W = W_c, lag = "civilwar_tm1") slx_plot_shock(fit, variable = "civilwar_tm1", unit = 1)data(defense_burden) W_c <- slx_weights(style = "custom", matrix = defense_burden$W_contig, row_standardize = FALSE) fit <- slx(ch_milex ~ milex_tm1 + civilwar_tm1, data = defense_burden$data, W = W_c, lag = "civilwar_tm1") slx_plot_shock(fit, variable = "civilwar_tm1", unit = 1)
A quick visual check on the structure of a weights matrix. For large
n the heatmap becomes noisy; use the max_n argument to subsample.
slx_plot_W(W, max_n = NULL, labels = NULL)slx_plot_W(W, max_n = NULL, labels = NULL)
W |
An |
max_n |
Optional integer; if |
labels |
Optional character vector of row/column labels. |
A ggplot object.
A thin, opinionated wrapper around common spdep weights constructors.
Returns a standardized slx_W object that carries both the sparse matrix
and the listw form used by downstream routines.
slx_weights( x = NULL, style = c("contiguity", "rook", "knn", "distance", "custom"), k = 5, threshold = NULL, row_standardize = TRUE, matrix = NULL, ... )slx_weights( x = NULL, style = c("contiguity", "rook", "knn", "distance", "custom"), k = 5, threshold = NULL, row_standardize = TRUE, matrix = NULL, ... )
x |
An |
style |
Weights style. One of |
k |
Number of neighbors for |
threshold |
Distance threshold for |
row_standardize |
Logical; row-standardize the matrix? Default
|
matrix |
A numeric or sparse |
... |
Passed to the underlying |
An object of class slx_W with elements W (sparse matrix),
listw (spdep listw object), style, and row_standardized.
# Custom weights matrix from bundled data data(defense_burden) W <- slx_weights(style = "custom", matrix = defense_burden$W_contig, row_standardize = FALSE) W # Contiguity weights from an sf polygon layer if (requireNamespace("sf", quietly = TRUE) && requireNamespace("spdep", quietly = TRUE)) { nc <- sf::st_read(system.file("shape/nc.shp", package = "sf"), quiet = TRUE) W_nc <- slx_weights(nc, style = "contiguity") W_nc }# Custom weights matrix from bundled data data(defense_burden) W <- slx_weights(style = "custom", matrix = defense_burden$W_contig, row_standardize = FALSE) W # Contiguity weights from an sf polygon layer if (requireNamespace("sf", quietly = TRUE) && requireNamespace("spdep", quietly = TRUE)) { nc <- sf::st_read(system.file("shape/nc.shp", package = "sf"), quiet = TRUE) W_nc <- slx_weights(nc, style = "contiguity") W_nc }
These methods make slx objects compatible with the broom and
modelsummary ecosystems.
## S3 method for class 'slx' tidy(x, conf.int = FALSE, conf.level = 0.95, ...) ## S3 method for class 'slx' glance(x, ...)## S3 method for class 'slx' tidy(x, conf.int = FALSE, conf.level = 0.95, ...) ## S3 method for class 'slx' glance(x, ...)
x |
An |
conf.int |
Logical; include confidence intervals? |
conf.level |
Confidence level. |
... |
Unused. |
tidy.slx() returns a tibble::tibble() with one row per model
coefficient (both direct and spatial-lag terms) and columns
term, estimate, std.error, statistic, and p.value. When
conf.int = TRUE, conf.low and conf.high columns are added.
glance.slx() returns a one-row tibble::tibble() summarizing the
overall fit, with columns r.squared, adj.r.squared, sigma,
statistic (F statistic), df, df.residual, nobs, and
n_lag_terms (the number of spatial-lag regressors in the model).
data(defense_burden) W <- slx_weights(style = "custom", matrix = defense_burden$W_contig, row_standardize = FALSE) fit <- slx(ch_milex ~ milex_tm1 + civilwar_tm1, data = defense_burden$data, W = W, lag = "civilwar_tm1") tidy(fit) glance(fit)data(defense_burden) W <- slx_weights(style = "custom", matrix = defense_burden$W_contig, row_standardize = FALSE) fit <- slx(ch_milex ~ milex_tm1 + civilwar_tm1, data = defense_burden$data, W = W, lag = "civilwar_tm1") tidy(fit) glance(fit)