knitr::opts_chunk$set( collapse = TRUE, comment = "#>", fig.width = 7, fig.height = 4.5, dpi = 96 ) set.seed(1)
Many actors in the social world --- armed groups, firms, diplomats, migrating populations --- have no fixed location. They move through space over time, and where two such actors interact is itself an outcome worth modeling. The Projected Actor Location (PALS) method [@kim2023pals] addresses this by projecting where a mobile actor "is" at any moment from the spatiotemporal history of its past interactions, using exponential-smoothing weights that favour recent and nearby events.
The palsr package implements the full workflow:
pal_events());estimate_pals());project_pals());predict_event_locations(), pal_distance());bootstrap_pals(), pool_rubin()).library(palsr)
For a focal actor $i$ at prediction time $t$, PALS forms a recency-weighted mean of the locations of $i$'s own past events (the focal component) and a recency-weighted mean of the locations of events involving $i$'s past interaction partners, or alters (the alter component). The projected location is a convex combination of the two, $$ g_i(t) = (1-\pi)\sum_e W_i(e)\, g(e) \;+\; \pi \sum_e W_k(e)\, g(e), \qquad \pi = \mathrm{logistic}(\gamma + \eta\, v), $$ where the weights decay with event age --- $W_i(e) \propto (\text{age}^{\alpha})^{-1}$ for the focal actor and analogously with $\beta$ for the alters --- and the mixing weight $\pi$ depends on how active the focal actor is relative to its alters through the event-count ratio $v$. The four parameters are therefore:
| Parameter | Role | |-----------|------| | $\alpha$ | decay of the focal actor's own history | | $\beta$ | decay of the alters' histories | | $\gamma$ | intercept of the focal-vs-alter mixing weight | | $\eta$ | dependence of the mixing weight on relative activity |
A reduced one-parameter model fixes $\pi = 0$ (focal history only) and estimates $\alpha$ alone; it is fast and surprisingly competitive.
The package ships a deterministic simulated dataset, nigeria_sim, of 1,500
dyadic conflict events among 25 mobile actors between 2000 and 2016, so that
examples run identically everywhere. The bundled nigeria_acled dataset provides
the real events from the replication archive of Kim, Liu and Desmarais (2023).
data(nigeria_sim) nigeria_sim summary(nigeria_sim)
You can build your own pal_events object from any data frame by naming the
actor, time, longitude and latitude columns:
raw <- data.frame( from = c("A", "A", "B"), to = c("B", "C", "C"), when = as.Date(c("2001-01-01", "2001-06-01", "2002-01-01")), x = c(7.1, 8.0, 7.5), y = c(9.0, 9.4, 10.1) ) pal_events(raw, actor1 = "from", actor2 = "to", time = "when", lon = "x", lat = "y")
Estimation marches forward through time: every event is predicted using only events strictly earlier than it, and the parameters minimize the mean great-circle (Haversine) distance between predicted and observed locations.
fit1 <- estimate_pals(nigeria_sim, model = "one") fit1 coef(fit1)
The full four-parameter model adds the alter component. We cap the optimizer iterations here purely to keep the vignette quick:
fit4 <- estimate_pals(nigeria_sim, model = "four", control = list(maxit = 60)) coef(fit4)
With a fitted model (or a hand-specified pals_params()), project where each
actor is at a given time:
pal_2015 <- project_pals(nigeria_sim, predict_time = as.Date("2015-01-01"), params = fit1) head(pal_2015)
library(ggplot2) ggplot(pal_2015, aes(lon, lat)) + geom_point(colour = "#2b6cb0", size = 2) + geom_text(aes(label = actor), vjust = -0.8, size = 3) + labs(title = "Projected actor locations, 2015-01-01", x = "Longitude", y = "Latitude") + theme_minimal()
Because the projection is recomputed as time advances, each actor traces a trajectory through space. Projecting a few actors at yearly intervals and drawing their paths over the cloud of observed events shows how PALS captures mobile actors drifting through the theatre:
actors <- c("G03", "G08", "G14", "G21") dates <- as.Date(sprintf("%d-01-01", seq(2005, 2016))) traj <- project_pals(nigeria_sim, actors = actors, predict_time = dates, params = fit1) traj <- traj[!is.na(traj$lon), ] ends <- do.call(rbind, lapply(split(traj, traj$actor), function(d) d[which.max(d$time), ])) ggplot() + geom_point(data = nigeria_sim, aes(lon, lat), colour = "grey80", size = 0.5, alpha = 0.5) + geom_path(data = traj, aes(lon, lat, colour = actor), linewidth = 0.8, arrow = grid::arrow(length = grid::unit(0.18, "cm"), type = "closed")) + geom_point(data = traj, aes(lon, lat, colour = actor), size = 1.6) + geom_text(data = ends, aes(lon, lat, colour = actor, label = actor), nudge_y = 0.35, size = 3, show.legend = FALSE) + scale_colour_brewer(palette = "Dark2", name = "Actor") + labs(title = "Projected actor trajectories, 2005-2016", x = "Longitude", y = "Latitude") + coord_quickmap() + theme_minimal()
The predicted location of an interaction between two actors is the mean of their two projected locations. Supplying observed coordinates scores the prediction in kilometres:
targets <- nigeria_sim[nigeria_sim$time > as.Date("2014-01-01"), ] scored <- predict_event_locations(nigeria_sim, targets, fit1) summary(scored$error_km)
The dyadic distance between two actors' projected locations is the key covariate for modeling who interacts with whom:
dyads <- data.frame(actor1 = "G01", actor2 = "G02", time = as.Date("2014-06-01")) pal_distance(nigeria_sim, dyads, fit1, transform = "log")
bootstrap_pals() resamples events with replacement and re-estimates the model
on each replicate, yielding bootstrap standard errors and percentile intervals.
(We use a small number of replicates here for speed; the paper uses ten.)
bt <- bootstrap_pals(nigeria_sim, R = 10, model = "one", seed = 1) summary(bt)
When a downstream estimand (say, a regression coefficient using PAL distances) is computed on each replicate, treat the replicates as multiple imputations and combine them with Rubin's Rules, which propagate both within- and between-replicate uncertainty:
q <- c(1.10, 0.95, 1.20, 1.05, 0.98) # per-replicate estimates u <- c(0.04, 0.05, 0.045, 0.038, 0.052) # per-replicate variances pool_rubin(q, u, df = TRUE, dfcom = 100)
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.