Welcome to the ResIN tutorial! ResIN stands for ‘Response Item Networks’ and was first introduced by Carpentras et.al. 2022. If you are interested in all things ResIN-related, including news, papers, and the corresponding Python tutorial, please check out the ResIN website.
This tutorial primarily introduces the ResIN-R package and provides an overview of the main functions, their capabilites, and additional features. It requires a little bit of background knowledge in R and the Tidyverse but certainly no advanced skills. For an in-depth conceptual introduction to ResIN as a method, please refer to the ResIN website or the above-referenced publication. The website also features upcoming dates for introductory talks performed by the ResIN team if you are instead interested in a live, hands-on tutorial.
Introducing ResIN
ResIN is a model of co-endorsements of social attitudes. Broadly understood, attitudes denote a sentiment towards a social or political issue, for instance, opposing cuts the minimum wage, being neutral on carbon taxes, or supporting equal pay for women and men. ResIN presumes that the more people co-endorse a given pair of attitudes (e.g. simultaneously supporting equal pay while opposing cuts to the minimum wage), the closer such attitudes are related to one another in a latent space.
More formally, ResIN models all possible interactions among a given set of attitudes as an network, where responses form the nodes and the edge-weights (or connecting links) determines the strength of the attractive force between them. Unlike classic belief network models, ResIN takes explicit advantage of the latent space in which attitude networks are embedded in. Given that only co-endorsed (i.e. positively correlated) nodes attract one another, a force simulation involving all survey responses (i.e. a force-directed algorithm) results in an interpretable, theoretically meaningful latent space in which both attitudes and people can be located.
To see this in action, let’s replicate and extend the analysis in Lüders et.al.’s 2024 piece, ‘Attitude networks as intergroup realities: Using network‐modelling to research attitude‐identity relationships in polarized political contexts.’
Data import and cleaning
As with any data analysis, a little bit of housekeeping goes a long way… and ResIN is no exception! Let’s use the Lüders et.al. (2024) data – which comes for free with the R-package – to illustrate this process.
First, lets select the core set of eight issue items from the Lüders et.al. data and, as in the paper, re-code a few of these such that agreement always denotes the liberal position. (This step is not necessary but makes interpretation of the the results easier). These attitudes deal with the legality of abortion, reducing income inequality, deporting illegal immigrants, increasing welfare spending, increase welfare spending, gay marriage, protecting the environment, gun control, and government aid to African Americans.
We’ll also give each item and their respective response options more telling labels to further improve visual interpretability.
## Sub-setting and re-coding items in a liberal-conservative direction
Core_Items <- BrJSocPsychol_2024 %>% dplyr::select(Q9_1, Q9_2, Q9_3, Q9_4,
Q9_5, Q9_6, Q9_7, Q9_8) %>%
dplyr::mutate(Q9_1 = recode(Q9_1, "Strongly Disagree" = "Strongly Agree",
"Somewhat Disagree" = "Somewhat Agree",
"Neutral" = "Neutral",
"Somewhat Agree" = "Somewhat Disagree",
"Strongly Agree" = "Strongly Disagree"),
Q9_3 = recode(Q9_3, "Strongly Disagree" = "Strongly Agree",
"Somewhat Disagree" = "Somewhat Agree",
"Neutral" = "Neutral",
"Somewhat Agree" = "Somewhat Disagree",
"Strongly Agree" = "Strongly Disagree"))
## Relabeling the attitudes
colnames(Core_Items) <- c("legal_abort", "equalize_incomes", "keep_immigrants",
"welfare_spending", "gay_marriage", "protect_environ",
"gun_control", "aid_blacks")
# Assigning response symbols for easier interpretation
Core_Items <- Core_Items %>%
mutate(across(everything(), ~ recode(.,
"Strongly Agree" = "++",
"Somewhat Agree" = "+",
"Neutral" = "+/-",
"Somewhat Disagree" = "-",
"Strongly Disagree" = "--",
)))
## Setting the seed for consistency
set.seed(42)
The cleaned data set now looks like this:
head(Core_Items)
#> # A tibble: 6 × 8
#> legal_abort equalize_incomes keep_immigrants welfare_spending gay_marriage
#> <chr> <chr> <chr> <chr> <chr>
#> 1 -- - - +/- --
#> 2 ++ ++ + ++ ++
#> 3 + + +/- + +/-
#> 4 + +/- ++ +/- -
#> 5 - + + + -
#> 6 ++ + ++ ++ ++
#> # ℹ 3 more variables: protect_environ <chr>, gun_control <chr>,
#> # aid_blacks <chr>
Create your first ResIN network!
Let’s get to the meat of it. For a first, bare-bones ResIN model, we
can simply supply our Core_Items
set to the
ResIN
function.
ResIN_out <- ResIN(Core_Items)
Not too bad! You’ll notice that the ResIN
function
produces and output a basic ggplot by default. This feature can be
turned of by setting plot_ggplot=FALSE
(something you
definitely want to do if you need to bootstrapping thousands of
networks, but we’ll get to that part later). You can always access and
call the ResIN-plot object in the stored output (e.g.,
ResIN_output$ResIN_ggplot
. To suppress the off-the-shelf
plot generation altogether, simply set
generate_ggplot=FALSE
.
If you would like to produce a less busy looking plot, you can
replace the node labels with a simpler geom_point
-aesthetic
by specifying plot_responselabels=FALSE
.
One feat you might have noticed from the above plot is that ResIN does not innately care if the politically left attitudes are also plotted on the left-hand side of the graph. In fact, the left-wing attitude cluster happens to appear on the right-hand-side in above plot.
If you are modeling concepts for which spatial orientation has a
particular meaning, such as political ideology, you can specify a
particular attitude node which should consistently appear on the left
side of the resulting graph with the left_anchor
argument.
Below, we’ll set the left_anchor = "legal_abort_++"
as
strongly wishing to preserve legal access to abortion appears as one of
the most left-wing attitude in the starting plot. (Setting a
left_anchor
does not affect the network estimation or
spatial positioning of the nodes - it simply flips the x-axis should the
target node appear on the right-hand half of the grapth). We’ll also set
a seed to avoid (minor) fluctuations in between runs.
first_ResIN <- ResIN(Core_Items, plot_responselabels=FALSE, plot_ggplot=FALSE, seed = 22, left_anchor = "legal_abort_++")
first_ResIN$ResIN_ggplot
Node coloring options & visual enhancements
So far so good. The above plots nicely reveal the familiar unidimensional spectrum in US politics with item responses in favor of welfare spending, protecting the environment, and protecting abortion rights on the left and their inverse positions on the political right. However, the graph also shows that the left-leaning attitude cluster appears much more consolidated; left-wingers (strongly) agree much more frequently and consistently than on the opposite end of the ideological aisle. But what about the moderates? Let’s try to reproduce figure 2a in Lüders et.al.’s (2024, p. 45) to get some more clarity on that.
ResIN_out <- ResIN(Core_Items, plot_whichstat = "choices",
response_levels = c("--", "-", "+/-" , "+", "++"),
plot_responselabels = FALSE,
plot_title = "BrJSocPsychol 2024 ResIN Network",
left_anchor = "legal_abort_++", seed = 22)
In the above function call, you’ll notice a couple of additional
arguments. The most important one, plot_whichstat
,
determines which piece of information we’d like to visually enhance. As
in Lüders’s figure 2a, we simply opted for the different response
choices by setting plot_whichstat="choices"
. Since R
applies an alpha-numeric coding for qualitative response levels, you can
supply the correct order (either from most to least or least to most
agreement) with the response_levels
argument. This should
be a character vector of the same length of the response levels – just
remember to put them in the correct order.
As in the original paper, we omit the response labels in favor of
dots for nodes. Finally, the plot_title
argument lets you
specify a custom title. Neat - ha?!
The above plot shows that the neutral nodes are far more often co-endorsed with the moderate-right and strong-right choice options. All in all, the right wing attitude space appears more mixed as it incorporates both neutral and even some of the moderate-left leaning response options. How about visualizing node clusters next?
Cluster detection and plotting
The ResIN()
function features several cluster detection
methods that are enabled by default. To turn this feature off, simply
set detect_clusters=FALSE
. By default, ResIN obtains latent
cluster assignments based on the leading eigenvector method
(cluster_method="cluster_leading_eigen"
) but any if the
cluster detection algorithms implemented in the the igrah-package can
be called. To avoid confusion with the above color scheme, we’ll specify
a different color_palette
below. The ResIN function
supports all default ggplot
scale-brewer palettes. Now, let’s see this in action.
## Using leading eigenvalue by default:
ResIN_out <- ResIN(Core_Items, detect_clusters = TRUE, plot_whichstat = "cluster",
plot_responselabels = FALSE, plot_title = "Leading eigenvalue community detection",
color_palette = "Set2", seed = 22, left_anchor = "legal_abort_++")
## Switching to edge-betweenness cluster detection:
ResIN_out <- ResIN(Core_Items, detect_clusters = TRUE, plot_whichstat = "cluster",
cluster_method = "cluster_fast_greedy", plot_responselabels = FALSE,
plot_title = "Fast and greedy community detection",
color_palette = "Set1", seed = 22, left_anchor = "legal_abort_++")
The above networks showcase how different community detection
algorithms can yield different solutions. However, both the
leading-eigenvalue and fast-greedy examples indicate that the moderate
node communities (1, and 4 above and 1 below) appears spatially more
proximate to the right-wing cluster which suggests a more similar
answering behavior among these groups of respondents. You can find the
node-level cluster assignments via
ResIN_out$ResIN_nodeframe$cluster
. The individual-level
probabilistic, cluster assignments can be accessed via the auxiliary
objects: ResIN_out$aux_objects$cluster_probabilities
. The
maximum probability assignments are contained in the
max_clusterprob
object stored in the same location.
head(ResIN_out$aux_objects$cluster_probabilities)
#> cluster_1 cluster_2 cluster_3
#> 1 0.750 0.250 0.000
#> 2 0.125 0.000 0.875
#> 3 0.875 0.125 0.000
#> 4 0.750 0.125 0.125
#> 5 1.000 0.000 0.000
#> 6 0.125 0.000 0.875
Node centrality
The ResIN()
function delivers off-the-shelve estimates
for various node-level statistics, including the strength, closeness,
and betweenness-centrality, as well as expected node influence. It also
estimates a couple of graph-level quantities including the global
clustering coefficient, average path length, and the network diameter.
These quantities are estimated by default; to turn this feature off,
simply specify network_stats=FALSE
. The example below
visualizes differences in strength centrality (it is possible to set
plot_whichstat
to either “Strength”, “Closeness”,
“Betweenness”, or “ExpectedInfluence”. The raw metrics for each
node-level statistic can be found in
ResIN_out\$ResIN_nodeframe
. Graph-level statistics are
stored in ResIN_out\$graph_stats
.
ResIN_out <- ResIN(Core_Items, plot_whichstat = "Strength", plot_responselabels = FALSE,
plot_title = "Node strength centrality", seed = 22, color_palette = "Greens",
left_anchor = "legal_abort_++")
Edge centrality and adjustments to the edge width
ResIN()
also natively supports visual adjustments to the
edge width or thickness on the basis of either the bi-variate
correlation weight (plot_edgestat = "weights"
) or the edge
betweenness centrality
(plot_edgestat = "cluster_edge_betweenness"
). The plot
below showcases the latter while also visualizing the clusters detected
with the edge betweenness algorithm.
ResIN_out <- ResIN(Core_Items, detect_clusters = TRUE, plot_whichstat = "cluster",
cluster_method = "cluster_edge_betweenness",
plot_edgestat = "edgebetweenness",
plot_responselabels = FALSE,
plot_title = "Edge weight based on edge-betweenness centrality",
seed = 22, color_palette = "Set1", left_anchor = "legal_abort_++")
Using covariates
The ability to visualize various covariates of interest at the node-level is one of ResIN’s most powerful features. Lüders et.al.’s (2024, p. 45) employed this strategy to co-visualize ResIN node position and the node-level partisan-affective polarization among their sample (figure 2b.).
To accomplish this with the ResIN function, we first need to attach
our covariates of interest to the attitude frame. Then, we simply need
to tell ResIN which data columns to treat as node variables by supplying
a dedicated node_vars
vector. Likewise we need to supply a
separate string, node_covars
, specifying the desired set of
node-level covariates. Finally, we have to tell ResIN with
node_costats
which covariate statistic to calculate for
which of the given covariates. Currently supported are
"mean"
, "median"
, "sd"
,
"var"
, and several other, base-R functions. Just make sure
that node_costats
has the same length as the
node_covars
so that ResIN knows which statistic to extract
based on which covariate.
Attached to the ResIN_nodeframe
, the resulting ResIN
object will feature a new set of variables with concatenated labels
consisting of the original co-variate name and the desired statistic
(e.g., ResIN_nodeframe$age_mean
). If you want to visualize
these node-level covariates, you simply need to supply the concatenated
covariate-statistic label to the plot_whichstat
argument.
The example below shows how to replicate the affective polarization
measure in Lüders et.al. (2024) following this strategy.
## Calculating the relative preference of Democrats over Republicans
##(Democrat feelings thermometer minus republican feelings thermometer)
Core_Items$dem_bias <- as.numeric(BrJSocPsychol_2024$Q15_1) - as.numeric(BrJSocPsychol_2024$Q15_2)
## Separately specifying attitude nodes and covariate here:
ResIN_out <- ResIN(Core_Items,
node_vars = c("legal_abort", "equalize_incomes", "keep_immigrants",
"welfare_spending", "gay_marriage", "protect_environ",
"gun_control", "aid_blacks"), node_covars = c("dem_bias"),
node_costats = c("mean"), plot_whichstat = "dem_bias_mean",
plot_responselabels = FALSE, left_anchor = "legal_abort_++",
plot_title = "Affective preference of Democrats over Republicans",
color_palette = "RdBu", seed = 22)
The above plot nicely visualizes the co-dependency between affective and issue polarization which characterizes contemporary American political attitude space. However, it also shows that the relationship is not entirely symmetric; while all left-wing attitudes are associated with approximately the same degree of affective bias, only select attitudes within the more diverse, center/ center-right attitude community are predominantly endorsed by affectively polarized individuals. Switching back to viewing the complete response labels, we’ll note that these respondents reject government aid to African Americans and environmental regulations in particular.
ResIN_out <- ResIN(Core_Items,
node_vars = c("legal_abort", "equalize_incomes", "keep_immigrants",
"welfare_spending", "gay_marriage", "protect_environ",
"gun_control", "aid_blacks"), node_covars = c("dem_bias"),
node_costats = c("mean"), plot_whichstat = "dem_bias_mean",
plot_responselabels = TRUE, left_anchor = "legal_abort_++",
plot_title = "Affective preference of Democrats over Republicans",
color_palette = "RdBu", seed = 22)
Spacial interpretation and individual latent space scores
Finally, let’s turn our attention towards the latent space in which
our ResIN network is embedded in. As indicated above, ResIN combines
belief network analysis with latent variable modeling in that the
spatial location of the attitudes can be interpreted as an underlying
latent spectrum. Following Lüders (2024) core hypothesis, if the
left-right location of the abstract network space correlates
substantially with people’s partisan identities and animosities, there
should be strong reason to believe that the ResIN network spatially
captures the liberal-conservative attitude spectrum. You can access the
node location on the main spatial axis simply via
ResIN_out\$ResIN_nodeframe\$x
.
## Further attaching partisan identification
Core_Items <- Core_Items %>% mutate(partisan = as.numeric(recode(BrJSocPsychol_2024$Q13,
"Democrat" = 2,
"Independent" = 1,
"Republican" = 0)))
ResIN_out <- ResIN(Core_Items, node_vars = c("legal_abort", "equalize_incomes",
"keep_immigrants", "welfare_spending",
"gay_marriage", "protect_environ",
"gun_control", "aid_blacks"),
node_covars = c("dem_bias", "partisan"), node_costats = c("mean", "mean"),
plot_ggplot = FALSE)
## Loading the psych package to run the correlation test.
library(psych)
## Partisanship
corr.test(ResIN_out$ResIN_nodeframe$x, ResIN_out$ResIN_nodeframe$partisan_mean)
#> Call:corr.test(x = ResIN_out$ResIN_nodeframe$x, y = ResIN_out$ResIN_nodeframe$partisan_mean)
#> Correlation matrix
#> [1] 0.86
#> Sample Size
#> [1] 40
#> These are the unadjusted probability values.
#> The probability values adjusted for multiple tests are in the p.adj object.
#> [1] 0
#>
#> To see confidence intervals of the correlations, print with the short=FALSE option
## Affective polarization
corr.test(ResIN_out$ResIN_nodeframe$x, ResIN_out$ResIN_nodeframe$dem_bias_mean)
#> Call:corr.test(x = ResIN_out$ResIN_nodeframe$x, y = ResIN_out$ResIN_nodeframe$dem_bias_mean)
#> Correlation matrix
#> [1] 0.88
#> Sample Size
#> [1] 40
#> These are the unadjusted probability values.
#> The probability values adjusted for multiple tests are in the p.adj object.
#> [1] 0
#>
#> To see confidence intervals of the correlations, print with the short=FALSE option
Pearson correlation with the node location in the latent space with partisanship and democratic feelings thermometer bias of 0.86 and 0.88, respectively strongly suggest that ResIN captures some of the essence of the spatial-ideological divisions among the American public.
But what about the respondents themselves? ResIN obtains scores for a
given individual by taking the average of the node positions among the
attitude nodes that this individual endorsed. (More fancy scoring
methods that handle missing responses natively are in the works and will
likely be published with the next package release.) The spatial scores
are stored in a n\*2
data-frame called
ResIN_out\$ResIN_scores
; the major axis scores appear as
scores_x
and the minor axis component as
scores_y
.
## Partisanship at the individual level
corr.test(Core_Items$partisan, ResIN_out$ResIN_scores$scores_x)
#> Call:corr.test(x = Core_Items$partisan, y = ResIN_out$ResIN_scores$scores_x)
#> Correlation matrix
#> [1] 0.57
#> Sample Size
#> [1] 402
#> These are the unadjusted probability values.
#> The probability values adjusted for multiple tests are in the p.adj object.
#> [1] 0
#>
#> To see confidence intervals of the correlations, print with the short=FALSE option
## Affective polarization at the individual level
corr.test(Core_Items$dem_bias, ResIN_out$ResIN_scores$scores_x)
#> Call:corr.test(x = Core_Items$dem_bias, y = ResIN_out$ResIN_scores$scores_x)
#> Correlation matrix
#> [1] 0.7
#> Sample Size
#> [1] 402
#> These are the unadjusted probability values.
#> The probability values adjusted for multiple tests are in the p.adj object.
#> [1] 0
#>
#> To see confidence intervals of the correlations, print with the short=FALSE option
While these correlations are somewhat weaker, they still support the same postulate.
Bootstrapping
What sets ResIN apart from other belief network models is its ability to determine node location in a theoretically interpretable latent space. As a non-parametric method, however, it cannot rely on closed-form probability distributions to derive uncertainty bounds for location estimates in the latent space. In fact, the same is true for all descriptive network statistics covered in this tutorial.
How can one nevertheless estimate the uncertainty around such quantities? The answer - as you might have guessed from the section header - is bootstrapping.
DYI implementations for bootstrapping quantities generated with novel
methods can be relatively daunting to, even for experienced statistical
programmers. Thankfully, the ResIN
package features a
native workflow in the form of three functions that much-simplifies this
process.
A canned bootstrapping procedure with ResIN
involves the
following three steps:
The
ResIN_boots_prepare()
function prepares a given ResIN-object for re-sampling. Additional parameters, such asn
andboots_type
determine the number of iterations and the basic type of bootstrapping analysis to be performed. Supported are"resample"
and"permute"
; the former can be used to estimate the uncertainty around a point estimate while the latter is reserved for deriving a null-hypothesis distributions for that parameter.As the name implies, the
ResIN_boots_execute()
function executes a prepped bootstrapping procedure. If theparallel
option is set toTRUE
, the function exploits CPU parallelism via thedoSNOW
engine. If detect_cores is also set toTRUE
, all available CPU cores will automatically be employed.Once the simulation has concluded, the
ResIN_boots_extract()
function delivers a handy search-and-grap utility for a particular quantity of interest. Simply specify a unique search term, such aswhat = "x_scores"
and the function will grab, concatenate, and return all instances of the desired quantity. For an even lazier information retrieval, setting thesummarize_results = TRUE
lets you summarize the distribution of the extracted quantity across all bootstrap iterations.
Let’s run a bootstrapping analysis to estimate the non-parametric uncertainty around the above estimated correlation coefficient between the attitude node location and the node-level mean partisan identification.
## Let's generate a new, more lean ResIN analysis by omitting network statistics calculations,
## plot generation, and individual-level scoring. This will optimize the execution time.
ResIN_out <- ResIN(Core_Items, node_vars = c("legal_abort", "equalize_incomes",
"keep_immigrants", "welfare_spending",
"gay_marriage", "protect_environ",
"gun_control", "aid_blacks"),
node_covars = c("partisan"), node_costats = c("mean"),
left_anchor = "legal_abort_++", network_stats = FALSE,
generate_ggplot = FALSE, plot_ggplot = FALSE,
ResIN_scores = FALSE, detect_clusters = FALSE)
ResIN_prepped <- ResIN_boots_prepare(ResIN_out, n = 1000, boots_type = "resample")
## Running the bootstrap might take a while; uncomment to try yourself
# ResIN_executed <- ResIN_boots_execute(ResIN_prepped, parallel = TRUE, n_cores = 2L)
#
# saveRDS(ResIN_executed, "ResIN_executed.RDS")
ResIN_executed <- readRDS("ResIN_executed.RDS")
## Extracting the mean level partisanship per node across all iterations
partisan_means <- ResIN_boots_extract(ResIN_executed, what = "partisan_mean")
## Extracting the node-level latent space coordinate across all iterations
x_postions <- ResIN_boots_extract(ResIN_executed, what = "x")
## Correlating each list element and storing results in a new vector
correlations <- list()
for(i in 1:length(partisan_means)){
correlations[[i]] <- cor(partisan_means[[i]], x_postions[[i]])
}
correlations <- unlist(correlations)
## Ignoring a handful few results where left and right are still flipped
correlations[correlations<(-0.75)] <- NA
summary(correlations)
#> Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
#> 0.8538 0.8577 0.8591 0.8592 0.8606 0.8676 63
## Let's plot the result with 95% CI lines:
correlations <- as.data.frame(correlations)
prob_lines <- quantile(correlations$correlations, c(0.025,0.5, 0.975), na.rm=TRUE)
library(ggplot2)
ggplot(correlations, aes(x = correlations))+
geom_density(fill = "lightblue")+
ggtitle("Density of bootstrapped correlations between node position and partisanship")+
labs(y = "Probability density", x = "Correlation between ResIN attitude node
position and average partisan identity")+
geom_vline(xintercept = prob_lines[1], color = "darkred",linetype = 2)+
geom_vline(xintercept = prob_lines[2], color = "black",linetype = 2, size = 1)+
geom_vline(xintercept = prob_lines[3], color = "darkred",linetype = 2)+
xlim(c(0.85, 0.87))+
theme_classic()
The bootstrapped distribution shows that the positions in latent space and partisan identities are indeed strongly and robustly correlated.
Export ResIN graphs to qgraph, igraph, and gephi
If you are familiar with some other software suits - either in R or
elsewhere - you’ll likely appreciate the conversion functions that ship
with the ResIN package. At the moment, ResIN_to_igraph()
,
ResIN_to_qgraph()
, and ResIN_to_gephi()
handle
conversion to igraph
, qgraph
, and Gephi,
respectively.
## Easily convert a ResIN object to igraph:
ResIN_igraph <- ResIN_to_igraph(ResIN_out)
class(ResIN_igraph)
#> [1] "igraph"
ResIN_qgraph <- ResIN_to_qgraph(ResIN_out)
class(ResIN_qgraph)
#> [1] "qgraph"
# ResIN_to_gephi(ResIN_out, file = "ResIN_gephi.csv")