--- title: "Projecting actor locations and modeling dyadic interactions with PALS" output: rmarkdown::html_vignette bibliography: palsr.bib vignette: > %\VignetteIndexEntry{Projecting actor locations and modeling dyadic interactions with PALS} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", fig.width = 7, fig.height = 4.5, dpi = 96 ) set.seed(1) ``` ## Overview 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: 1. build a validated table of dyadic events (`pal_events()`); 2. estimate the smoothing parameters by minimizing great-circle prediction error (`estimate_pals()`); 3. project actor locations at arbitrary times (`project_pals()`); 4. predict dyadic interaction locations and build distance covariates (`predict_event_locations()`, `pal_distance()`); 5. quantify uncertainty with a nonparametric bootstrap and pool with Rubin's Rules (`bootstrap_pals()`, `pool_rubin()`). ```{r load} library(palsr) ``` ## The method in brief 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. ## A worked example 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). ```{r data} 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: ```{r build} 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") ``` ### Estimating the parameters 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. ```{r fit-one} 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: ```{r fit-four} fit4 <- estimate_pals(nigeria_sim, model = "four", control = list(maxit = 60)) coef(fit4) ``` ### Projecting actor locations With a fitted model (or a hand-specified `pals_params()`), project where each actor is at a given time: ```{r project} pal_2015 <- project_pals(nigeria_sim, predict_time = as.Date("2015-01-01"), params = fit1) head(pal_2015) ``` ```{r map, fig.alt = "Projected actor locations on 2015-01-01"} 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: ```{r trajectory-map, fig.width = 7, fig.height = 4.5, fig.alt = "Projected trajectories of four actors over 2005-2016"} 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() ``` ### Predicting interaction locations and dyadic distances 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: ```{r predict-events} 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: ```{r distance} dyads <- data.frame(actor1 = "G01", actor2 = "G02", time = as.Date("2014-06-01")) pal_distance(nigeria_sim, dyads, fit1, transform = "log") ``` ## Uncertainty: bootstrap and Rubin's Rules `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.) ```{r bootstrap} 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: ```{r rubin} 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) ``` ## References