3 Between partner SAM
Here we modify the within partner SAM for a between partner study design without repeated measures within partners. As explained in the main text [Martin and Jaeggi (2021); see Eq 3.2], this model can be considered as a simplification of the within partner model to remove feedback effects within partners (i.e. \(t=1\)). We therefore avoid repeating the detailed commentary on the formal and computational aspects of the model provided in the previous chapter. In contrast to within partner SAM, which fails to properly estimate assortment (2.6), a between partner SAM can be used to effectively partition assortment on SRN parameters using within-individual centering.
The between partner SAM for a measurement \(i\) of aggression \(z_{ijt=1}\) in focal individual \(j\) during a single interaction period \(t=1\) is given by
\[z_{ijt=1} = \mu_0 + \eta_{ijt=1} + \epsilon_{ijt=1}\] \[\eta_{ijt=1} = \mu_j + \left( \psi_1 + \psi_j \right)\mu_k'\] \[\mu_j = \mu_{\mathrm{A}j} + \mu_{\mathrm{E}j}, \quad \psi_j = \psi_{\mathrm{A}j} + \psi_{\mathrm{E}j}\] Note that the SRN measurement simply reduces to the residual trait value \(\epsilon_{ijt=1}\) because there is no temporal information for partitioning residual feedback effects. When there individuals are non-randomly distributed across social partners (e.g. due to assortment), the SRN trait value should instead be decomposed into within \(\eta_{W}\) and between \(\eta_{B}\) partner components to adjust for unbalanced sampling
\[\eta_{Wijt=1} = \mu_j + \left( \psi_1 + \psi_j \right) \left( \mu_k' - \bar{\mu}'_K \right)\] \[\eta_{Bijt=1} =\left( \psi_1 + \psi_j \right) \bar{\mu}'_K \] where \(\bar{\mu}'_K\) is the average SRN intercept of an individual’s social partners. To appropriately adjust the intrinsic SRN parameters for the between-partner component, we need to introduce an additional regression parameter \(\beta_{B}\) so that
\[z_{ijt=1} = \mu_0 + \eta_{Wijt=1} + \beta_{B}\eta_{Bijt=1} + \epsilon_{ijt=1}\]
The equivalent specification can be given for the social partner’s aggression measure \(z_{ikt=1}'\)
\[z_{ikt=1}' = \mu_0 + \eta_{Wikt=1}' + \beta_{B}\eta_{Bikt=1}' + \epsilon_{ikt=1}'\] \[\eta_{Wikt=1}' = \mu_k' + \left( \psi_1 + \psi_k' \right) \left( \mu_j - \bar{\mu}_J \right) \] \[\eta_{Bikt=1}' = \mu_k' + \left( \psi_1 + \psi_k' \right) \bar{\mu}_J\] \[\mu_k' = \mu_{\mathrm{A}k}' + \mu_{\mathrm{E}j}', \quad \psi_k' = \psi_{\mathrm{A}k}' + \psi_{\mathrm{E}k}'\] The random effects are assumed to be well-described by multivariate normal distributions.
\[\begin{bmatrix} \boldsymbol{\mu_{\mathrm{A}}}, \boldsymbol{\mu'_{\mathrm{A}}},\boldsymbol{\psi_{\mathrm{A}}},\boldsymbol{\psi}'_{\mathrm{A}} \end{bmatrix}^{\mathrm{T}} \sim \mathrm{MVNormal}(\boldsymbol{0}, \boldsymbol{\mathrm{G}} \otimes \boldsymbol{\mathrm{A}} ) \] \[\begin{bmatrix} \boldsymbol{\mu_{\mathrm{E}}}, \boldsymbol{\mu'_{\mathrm{E}}},\boldsymbol{\psi_{\mathrm{E}}},\boldsymbol{\psi}'_{\mathrm{E}} \end{bmatrix}^{\mathrm{T}} \sim \mathrm{MVNormal}(\boldsymbol{0}, \boldsymbol{\mathrm{E}} \otimes \boldsymbol{\mathrm{I}} ) \] \[\begin{bmatrix} \boldsymbol{\epsilon}, \boldsymbol{\epsilon}' \end{bmatrix}^{\mathrm{T}} \sim \mathrm{MVNormal}(\boldsymbol{0}, \boldsymbol{\mathrm{\Sigma}} ) \] We also assume that the social reaction norm (SRN) intercept and slope (co)variances are equivalent for focal (\(\boldsymbol{\mu},\boldsymbol{\psi}\)) and social partners (\(\boldsymbol{\mu}',\boldsymbol{\psi}'\)). The \(\boldsymbol{G}\) matrix can therefore be reduced to a 2x2 matrix for all individuals in the population
\[\boldsymbol{\mathrm{G}}= \begin{bmatrix} \mathrm{var([\boldsymbol{\mu},\boldsymbol{\mu}'])} & \mathrm{cov([\boldsymbol{\mu},\boldsymbol{\mu}'],[\boldsymbol{\psi},\boldsymbol{\psi}'])} \\ \mathrm{cov([\boldsymbol{\psi},\boldsymbol{\psi}'],[\boldsymbol{\mu},\boldsymbol{\mu}'])} & \mathrm{var([\boldsymbol{\psi},\boldsymbol{\psi}'])} \end{bmatrix}\]
The residual matrix \(\boldsymbol{\Sigma}\) estimates the association among focal and social partners’ residuals.
\[\boldsymbol{\Sigma}= \begin{bmatrix} \mathrm{var(\boldsymbol{\epsilon})} & \mathrm{cov}(\boldsymbol{\epsilon},\boldsymbol{\epsilon}') \\ \mathrm{cov}(\boldsymbol{\epsilon}',\boldsymbol{\epsilon}) & \mathrm{var(\boldsymbol{\epsilon'})} \end{bmatrix}\] This model is, therefore, appropriate for situations where the distinction between focal and partner is semi-arbitrary, e.g. when measuring within-sex interactions or when males and females exhibit similar patterns of phenotypic variation. In this case, we make the latter assumption for simplicity. To account for differences between the responses of focal individuals and social partners, the model can simply be extended with additional parameters, e.g. specifying separate \(G_M\) and \(G_F\) matrices for males and female respective genetic (co)variances and so on.
3.1 Simulate data
We now need to simulate interactions across multiple partners. Fortunately, we follow the same basic approach as the previous chapter, including use of the the custom pedfun()
function introduced in Ch. 1.5 for generating a genetic VCV. For heuristic purposes, we can consider our simulation as capturing interactions and assortment of each individual with four lifetime partners.
library(mvtnorm)
#common settings
= 4 #partners/individual
I_partner = 1 #observations/individual/seasonal partner
I_obs = I_partner*I_obs #samples/individual
I_sample
#population properties
= 300 #total individuals for simulation
I =400
popmin=600
popmax= 10
ngenerations <-sample(popmin:popmax, ngenerations, replace=TRUE) #N / generation
nids= sample(seq(0.15, 0.25,by=0.05),1) #extra-pair mating
epm = sample(seq(0.4,0.6,by=0.05),1) #proportion of non-breeding / generation
nonb
#relatedness matrix
<- pedfun(popmin=popmin, popmax=popmax, ngenerations=ngenerations,
A_mat epm=epm, nonb=nonb, nids=nids, I=I, missing=FALSE)
#####################################################################
#Parameter values
#####################################################################
= 0 #global intercept
alpha_0 = -0.5 #population interaction coefficient
psi_1 = 0.5 #residual feedback coefficient (epsilon_j ~ epsilon_t-1k)
phi = 0.3 #standard deviation of SRN intercepts
SD_intercept = 0.3 #SD of SRN slopes
SD_slope = 0.3 #assortment coefficient (expressed as correlation)
r_alpha = 0 #genetic correlation of random intercepts and slopes
r_G = 0 #environmental correlation
r_E = -0.3 #residual effect correlation (epsilon_tj = epsilon_tk)
r_R = 0.3 #genetic variance of REs
V_G = 0.3 #genetic variance of REs
V_E = 1
res_V
#Random effect correlations
<- matrix(c(1,r_G,r_G,1), nrow=2, ncol=2) #mu_A, beta_A
G_cor <- c(sqrt(V_G),sqrt(V_G)) #G effect sds
G_sd <- diag(G_sd) %*% G_cor %*% diag(G_sd)
G_cov
<- matrix(c(1,r_E,r_E,1), nrow=2, ncol=2) #mu_E, beta_E
E_cor <- c(sqrt(V_E),sqrt(V_E)) #E effect sds
E_sd <- diag(E_sd) %*% E_cor %*% diag(E_sd)
E_cov
#matrices
<- G_cov %x% A_mat
G_block <- E_cov %x% diag(1,I)
E_block
#generate correlated REs
<- rmvnorm(1, mean=rep(0,I*2), sigma=G_block)
Gvalues = data.frame(matrix(Gvalues, nrow=I, ncol=2))
G_val cor(G_val)
## X1 X2
## X1 1.000000000 -0.003026306
## X2 -0.003026306 1.000000000
<- rmvnorm(1, mean=rep(0,I*2), sigma=E_block)
Evalues = data.frame(matrix(Evalues, nrow=I, ncol=2))
E_val cor(E_val)
## X1 X2
## X1 1.00000000 0.02323685
## X2 0.02323685 1.00000000
#combine temporary object for all SRN parameters
#use shorthand mu = 0, psi = 1
= cbind(G_val,E_val)
P colnames(P) = c("A0", "A1", "E0", "E1")
#individual phenotypic REs
#use shorthand mu = 0, psi = 1
$P0 = P$A0 + P$E0
P$P1 = P$A1 + P$E1
P
#add ID
$ID = seq(1:I) P
The assortment procedure is fundamentally the same but now considers multiple partners. For demonstrative purposes, we now assume that assortment occurs for SRN slopes rather than intercepts, which directly assesses whether the model can appropriately partition variation due to plasticity and assortment.
library(dplyr)
library(MASS)
= list()
pairs for (j in 1:I_partner){
#male RN intercept (x I_partner for multiple lifetime partners)
<- data.frame(P1_m = P$P1[1:(I/2)], ID_m = (1:(I/2)) )
sort.m <-sort.m[order(sort.m[,"P1_m"]),]
sort.m#female phenotypic RN slopes
<- data.frame(P1_f = P$P1[(I/2 + 1):I], ID_f = ((I/2+1):I) )
sort.f <-sort.f[order(sort.f[,"P1_f"]),]
sort.f#generate random dataset with desired rank-order correlation
<- matrix(r_alpha, ncol = 2, nrow = 2) #cor of male and female values
temp_mat diag(temp_mat) <- 1 #cor matrix
#sim values
<- mvrnorm(n = I/2, mu = c(0, 0), Sigma = temp_mat, empirical=TRUE)
temp_data1#ranks of random data
<- rank(temp_data1[ , 1], ties.method = "first")
rm <- rank(temp_data1[ , 2], ties.method = "first")
rf #induce cor through rank-ordering of RN vectors
cor(sort.m$P1_m[rm], sort.f$P1_f[rf])
#sort partner ids into dataframe
= data.frame(ID_m = sort.m$ID_m[rm], ID_f = sort.f$ID_f[rf])
partner.id = partner.id[order(partner.id[,"ID_m"]),]
partner.id #add to list
= partner.id
pairs[[j]]
}
= bind_rows(pairs)
partner.id = partner.id[order(partner.id$ID_m),]
partner.id
#put all dyads together
$dyadn = seq(1:nrow(partner.id))
partner.id
#add values back to dataframe (male and joint)
$P0m <- P$P0[match(partner.id$ID_m,P$ID)]
partner.id$P0f <- P$P0[match(partner.id$ID_f,P$ID)]
partner.id$P1m <- P$P1[match(partner.id$ID_m,P$ID)]
partner.id$P1f <- P$P1[match(partner.id$ID_f,P$ID)]
partner.id
$A0m <- P$A0[match(partner.id$ID_m,P$ID)]
partner.id$A0f <- P$A0[match(partner.id$ID_f,P$ID)]
partner.id$A1m <- P$A1[match(partner.id$ID_m,P$ID)]
partner.id$A1f <- P$A1[match(partner.id$ID_f,P$ID)]
partner.id
$E0m <- P$E0[match(partner.id$ID_m,P$ID)]
partner.id$E0f <- P$E0[match(partner.id$ID_f,P$ID)]
partner.id$E1m <- P$E1[match(partner.id$ID_m,P$ID)]
partner.id$E1f <- P$E1[match(partner.id$ID_f,P$ID)]
partner.id
#check correlation again
cor(partner.id$P1m, partner.id$P1f)
## [1] 0.2989723
We can now calculate observed aggression measures for a single observation within each dyad.
#number of dyads
= nrow(partner.id)
ndyad = partner.id #congruent with repeated measures code
pair_df
#correlated residuals between male and females
<- matrix(c(1,r_R,r_R,1), nrow=2, ncol=2)
R_cor <- sqrt(res_V)
res_sd <- diag(c(res_sd,res_sd)) %*% R_cor %*% diag(c(res_sd,res_sd))
R_cov <-data.frame(rmvnorm(nrow(pair_df), c(0,0), R_cov))
res_ind$resAGm = res_ind$X1
pair_df$resAGf = res_ind$X2
pair_df
#add interaction number
$turn = rep(1,ndyad)
pair_df
#males
#eta_j{t=1} = mu_j + (psi + psi_j)*mu_k
$turn==1,"eta_m"] = pair_df[pair_df$turn==1,"P0m"] +
pair_df[pair_df+ pair_df[pair_df$turn==1,"P1m"])*(pair_df[pair_df$turn==1,"P0f"])
(psi_1
#females
#eta_k{t=1} = mu_k +(psi + psi_k)*mu_j
$turn==1,"eta_f"] = pair_df[pair_df$turn==1,"P0f"] +
pair_df[pair_df+ pair_df[pair_df$turn==1,"P1f"])*(pair_df[pair_df$turn==1,"P0m"])
(psi_1
#add intercept and residuals (other fixed effects could be added here as well)
$AG_m = alpha_0 + pair_df$eta_m + pair_df$resAGm
pair_df$AG_f = alpha_0 + pair_df$eta_f + pair_df$resAGf
pair_df
#create indices for STan
= I/2 #number of males
Im = I/2 #number of females
If = (I/2)*I_partner #total observations per sex
N_sex <-pair_df$ID_m #male ID
idm<-pair_df$ID_f #female ID
idf<-idf - Im #within female vector
idf
#all partners for each individual
#partner IDs for male individuals
<-data.frame(idfocal = rep(1:(I/2)), #all partners ID
partners_mpartner1 = NA, partner2 = NA, partner3 = NA, partner4 = NA)
for(i in 1:(I/2)){partners_m[i,c(2:5)] <-partner.id[partner.id$ID_m==i,"ID_f"]}
#partner IDs for female individuals
<-data.frame(idfocal = rep((I/2+1):I), #all partners ID
partners_fpartner1 = NA, partner2 = NA, partner3 = NA, partner4 = NA)
for(i in (I/2+1):I){partners_f[i-(I/2),c(2:5)] <-partner.id[partner.id$ID_f==i,"ID_m"]}
#data prep for Stan
<-
stan_data list(N_sex = N_sex, I = I, Im=Im, If = If, idm = idm, idf = idf, partners_m = partners_m, partners_f = partners_f,
AG_m = pair_df$AG_m, AG_f = pair_df$AG_f, time = pair_df$turn, A = A_mat)
It may also be of interest to calculate the true average partner values that we’ll need to center the statistical model.
#calculate mean partner phenotype for each subject
#average female for male partners
<- aggregate(P0f ~ ID_m, mean, data = partner.id)
mean_0m names(mean_0m)[2] <- "meanP0m"
<- aggregate(P1f ~ ID_m, mean, data = partner.id)
mean_1m names(mean_1m)[2] <- "meanP1m"
$meanP0m <- mean_0m$meanP0m[match(partner.id$ID_m,mean_0m$ID_m)]
partner.id$meanP1m <- mean_1m$meanP1m[match(partner.id$ID_m,mean_1m$ID_m)]
partner.id
#average male for female partners
<- aggregate(P0m ~ ID_f, mean, data = partner.id)
mean_0f names(mean_0f)[2] <- "meanP0f"
<- aggregate(P1m ~ ID_f, mean, data = partner.id)
mean_1f names(mean_1f)[2] <- "meanP1f"
$meanP0f <- mean_0f$meanP0f[match(partner.id$ID_f,mean_0f$ID_f)]
partner.id$meanP1f <- mean_1f$meanP1f[match(partner.id$ID_f,mean_1f$ID_f)] partner.id
For empirical data where the true partner values are unknown, we won’t want to average raw partner values subject to measurement error, but instead estimate them directly in our Stan model.
3.2 Estimate the model
To code the model, we begin by simplifying the within partner model to remove SRN and residual feedback from the male and female responses, and then we expand it by adding new code for the within-individual centering procedure.
write("
data {
//indices and scalars used for model specification
int<lower=1> N_sex; //total aggression observations per sex (I/2 * 4 lifetime partners)
int<lower=0> I; //total individuals (M + F)
int<lower=0> Im; //number of males
int<lower=0> If; //number of females
int<lower=1> idm[N_sex]; //index of male AG observations (of length N_sex)
int<lower=1> idf[N_sex]; //index of female AG observations
int<lower=1> partners_m [Im,5]; //index of male partner IDs, first column is focal ID (1 + 4 IDs)
int<lower=1> partners_f [If,5]; //index of female partner IDs, first column is focal ID (1 + 4 IDs)
//empirical data
matrix[I,I] A; //relatedness matrix
real AG_m[N_sex]; //male aggression measurements
real AG_f[N_sex]; //female aggression measurements
real time[N_sex]; //time index (=1 for all measures)
}
transformed data{
matrix[I,I] LA = cholesky_decompose(A); //lower-triangle A matrix
}
parameters {
//population effects
real alpha_0; //aggression global intercept
real psi_1; //expected interaction coefficient
real beta_B; //slope of between-partner component
//no way to partition feedback when t=1
//real<lower=-1,upper=1> phi; //(-1,1) ensures unique solution
//random effects (standard deviations)
vector<lower=0, upper = 1>[2] sd_P; //phenotypic SRN mu & psi SDs
vector<lower=0, upper = 1>[2] sd_R; //male & female residual SDs
cholesky_factor_corr[2] LG; //genetic SRN correlations
cholesky_factor_corr[2] LE; //permanent environmental SRN correlations
cholesky_factor_corr[2] LR; //sex-specific residual correlations
matrix[I,2] std_devG; //individual-level unscaled G SRN deviations
matrix[I,2] std_devE; //individual-level unscaled E SRN deviations
//SRN heritability parmameters, i.e. Var(G_RN) / Var(P_RN)
//see supplementary appendix SI for further explanation of this parameter
vector<lower=0,upper=1>[2] SRN_h2;
}
transformed parameters {
vector<lower=0>[2] sd_G; //SDs of G effects (derived from sd_P)
vector<lower=0>[2] sd_E; //SDs of E effects (derived from sd_P)
matrix[I,2] SRN_P; //scaled P SRN parameter deviations
matrix[I,2] SRN_G; //scaled G SRN parameter deviations
matrix[I,2] SRN_E; //scaled E SRN parameter deviations
matrix[If, 2] partner_meanm; //average SRN parameters of males' partners
matrix[Im, 2] partner_meanf; //average SRN parameters of females' partners
//standard deviations of genetic effects
//simplified from sqrt ( total RN phenotype variance * h2 )
sd_G[1] = sd_P[1] * sqrt(SRN_h2[1]); //genetic SD for RN intercepts
sd_G[2] = sd_P[2] * sqrt(SRN_h2[2]); //genetic SD for RN slopes
//standard deviations of environmental effects (total phenotype SD * proportion environment SD)
sd_E[1] = sd_P[1] * sqrt(1 - SRN_h2[1]); //environment SD for RN intercepts
sd_E[2] = sd_P[2] * sqrt(1 - SRN_h2[2]); //environment SD for RN slopes
//matrix normal parameterization of Kronecker product between G and A
SRN_G = LA * std_devG * diag_pre_multiply(sd_G, LG)' ;
//non-centered parameterization of permanent environmental effects
SRN_E = std_devE * diag_pre_multiply(sd_E, LE)';
//phenotypic RN effects (P = G + E); here G = additive genetic effects
SRN_P = SRN_G + SRN_E;
//calculate the mean SRN parameters of each male's lifetime partners
for(i in 1:Im) partner_meanm[i] = [mean(col(SRN_P[partners_m[i,2:5]],1)),
mean(col(SRN_P[partners_m[i,2:5]],2))];
//calculate the mean SRN parameters of each female's lifetime partners
for(i in 1:If) partner_meanf[i] = [mean(col(SRN_P[partners_f[i,2:5]],1)),
mean(col(SRN_P[partners_f[i,2:5]],2))];
}
model{
//separate male and female vectors for efficiency
matrix[Im,2] SRN_Pm = SRN_P[1:Im]; //male SRN phenotypic deviations
matrix[If,2] SRN_Pf = SRN_P[(Im+1):I]; //female SRN phenotypic deviations
//separate SRN intercepts and slopes (phenotypic deviations)
vector[Im] mu_m = col(SRN_Pm,1); //SRN intercepts
vector[If] mu_f = col(SRN_Pf,1);
vector[Im] psi_m = col(SRN_Pm,2); //SRN slopes
vector[If] psi_f = col(SRN_Pf,2);
//separate mean partner SRN intercepts and slopes
vector[Im] mu_meanm = col(partner_meanm,1); //mean partner SRN intercept for males
vector[If] mu_meanf = col(partner_meanf,1); //...for females
vector[Im] psi_meanm = col(partner_meanm,2); //mean partner SRN slope for males
vector[If] psi_meanf = col(partner_meanf,2); //...for females
//add in new vectors for within-individual centering
vector[N_sex] eta_Wm; //male SRN trait value
vector[N_sex] eta_Wf; //female SRN trait value
vector[N_sex] eta_Bm; //individual male SRN trait value toward average partner
vector[N_sex] eta_Bf; //individual female SRN trait toward average partner
//other components of linear predictor
vector[N_sex] linpred_m; //total expected value for male responses
vector[N_sex] linpred_f; //total expected value for female responses
vector[N_sex] epsilon_m; //residual for male responses
vector[N_sex] epsilon_f; //residual for female responses
//Male and female aggression response model
for (n in 1:N_sex) {
//SRN trait values (within-individual centered)
//there is no residual feedback, so only t = 1
//within-male male eta[t=1] = mu_j + (psi + psi_j)*(mu_k - mean_mu_K)
eta_Wm[n] = mu_m[idm[n]] + (psi_1 + psi_m[idm[n]])*(mu_f[idf[n]] - mu_meanm[idm[n]]) ;
//female eta[t=1] = mu_k + (psi + psi_k)*(mu_j - mean_mu_J)
eta_Wf[n] = mu_f[idf[n]] + (psi_1 + psi_f[idf[n]])*(mu_m[idm[n]] - mu_meanf[idf[n]]);
//average time=1 individual eta (used for feedback process)
//between male eta[t=1] = mu_j + psi_j*mean_mu_K
eta_Bm[n] = (psi_1 + psi_m[idm[n]])*mu_meanm[idm[n]];
//between female eta[t=1] = mu_k + psi_k*mean_mu_J
eta_Bf[n] = (psi_1 + psi_f[idf[n]])*mu_meanf[idf[n]];
//add global intercept and between-individual parameters to linear predictor
//other fixed effects can also be added here
linpred_m[n] = alpha_0 + eta_Wm[n] + beta_B*eta_Bm[n] ;
linpred_f[n] = alpha_0 + eta_Wf[n] + beta_B*eta_Bf[n] ;
//there is no residual feedback, so only t = 1
epsilon_m [n] = AG_m[n] - linpred_m[n];
epsilon_f [n] = AG_f[n] - linpred_f[n];
//correlated residuals between partners
[epsilon_m[n],epsilon_f[n]]' ~ multi_normal_cholesky([0,0], diag_pre_multiply(sd_R, LR));
}
//model priors
//fixed effects
alpha_0 ~ std_normal();
psi_1 ~ std_normal();
beta_B ~ std_normal();
//random effects
to_vector(sd_P) ~ cauchy(0,1);
to_vector(sd_R) ~ cauchy(0,1);
LG ~ lkj_corr_cholesky(2);
LE ~ lkj_corr_cholesky(2);
LR ~ lkj_corr_cholesky(2);
to_vector(std_devG) ~ std_normal();
to_vector(std_devE) ~ std_normal();
//reaction norm heritability
to_vector(SRN_h2) ~ beta(1.2,1.2);
}
generated quantities{
//cor and cov matrices of SRN parameters and residuals
matrix[2,2] Gcor = LG * LG'; //G SRN correlation matric
matrix[2,2] Ecor = LE * LE'; //E SRN correlation matric
matrix[2,2] Rcor = LR * LR'; //residual correlation matrix
matrix[2,2] Rcov = diag_matrix(sd_R)*Rcor*diag_matrix(sd_R); //residual covariance
matrix[2,2] Gcov = diag_matrix(sd_G)*Gcor*diag_matrix(sd_G); //G SRN covariance
matrix[2,2] Ecov = diag_matrix(sd_E)*Ecor*diag_matrix(sd_E); //E SRN covariance
matrix[2,2] Pcov = Gcov + Ecov; //P SRN covariance
matrix[2,2] Pcor = inverse(diag_matrix(sd_P))*Pcov*inverse(diag_matrix(sd_P)); //P SRN correlation
//variances
vector<lower=0>[2] V_P = sd_P .* sd_P;
vector<lower=0>[2] V_G = sd_G .* sd_G;
vector<lower=0>[2] V_E = sd_E .* sd_E;
vector<lower=0>[2] V_R = sd_R .* sd_R;
}", "sam3_2.stan")
As a reminder, this is the structure of the data list necessary for estimating this model.
str(stan_data)
## List of 12
## $ N_sex : num 600
## $ I : num 300
## $ Im : num 150
## $ If : num 150
## $ idm : int [1:600] 1 1 1 1 2 2 2 2 3 3 ...
## $ idf : num [1:600] 11 21 11 145 60 36 81 105 146 137 ...
## $ partners_m:'data.frame': 150 obs. of 5 variables:
## ..$ idfocal : int [1:150] 1 2 3 4 5 6 7 8 9 10 ...
## ..$ partner1: int [1:150] 161 210 296 187 178 227 184 190 159 258 ...
## ..$ partner2: int [1:150] 171 186 287 175 233 157 247 172 199 200 ...
## ..$ partner3: int [1:150] 161 231 254 192 229 269 211 151 299 252 ...
## ..$ partner4: int [1:150] 295 255 202 285 161 216 288 204 213 198 ...
## $ partners_f:'data.frame': 150 obs. of 5 variables:
## ..$ idfocal : int [1:150] 151 152 153 154 155 156 157 158 159 160 ...
## ..$ partner1: int [1:150] 8 24 16 25 59 80 6 37 9 15 ...
## ..$ partner2: int [1:150] 12 51 18 35 65 91 51 83 74 44 ...
## ..$ partner3: int [1:150] 97 57 95 109 105 141 90 95 102 104 ...
## ..$ partner4: int [1:150] 145 110 139 122 134 142 106 145 149 130 ...
## $ AG_m : num [1:600] -0.339 -0.522 -1.615 -2.357 1.469 ...
## $ AG_f : num [1:600] 2.397 -0.127 2.421 0.433 -3.509 ...
## $ time : num [1:600] 1 1 1 1 1 1 1 1 1 1 ...
## $ A : num [1:300, 1:300] 1 0.0768 0.0328 0.0349 0.0657 ...
## ..- attr(*, "dimnames")=List of 2
## .. ..$ : chr [1:300] "ID4251" "ID4702" "ID4783" "ID4670" ...
## .. ..$ : chr [1:300] "ID4251" "ID4702" "ID4783" "ID4670" ...
Let’s now estimate the model.
library(rstan)
.2 = stan_model("sam3_2.stan")
sam_3
.2 <- sampling(sam_3.2, data=stan_data, init = 0, warmup=1500, iter = 3000,
stan_results3chains=4, cores=4, control=list(adapt_delta=0.90) )
library(bayesplot)
mcmc_areas(stan_results3.2, pars = c( "psi_1", "beta_B", "V_P[1]", "V_P[2]", "Gcor[2,1]", "Ecor[2,1]", "Rcor[2,1]",
"Pcor[2,1]", "SRN_h2[1]", "SRN_h2[2]"), prob = 0.9 )
Overall, it seems the model is doing a good job of detecting the population SRN slope (-0.5), albeit with some uncertainty, as well as the range of the phenotypic variance of SRN intercepts and slopes (0.6), the phenotypic correlation between SRN parameters (0.3), and the SRN heritability of intercepts and slopes (0.5).
3.3 Estimating assortment
The mean partner intrinsic trait values calculated in the model can then be used to estimate the assortment coefficient of interest \(\beta_{{\bar{\psi}'}\psi}\), as well as the broader assortment matrix \(\boldsymbol{B_{\alpha}}\). We can directly calculate the quantities of the assortment matrix using the vectors of mean partner SRN parameters that we constructed and estimated with the Stan model. In this case, we’re also interesting in estimating the expected assortment within any particular breeding season, under the assumption that variation in assortment coefficients between seasons is random. To do this, we need to scale the (co)variances of interest appropriately for the expected variance of a single partner, rather than the mean of multiple partners. If the intrinsic SRN parameter values of social partners are independent Gaussian variables, then the expected variance for a single partner phenotype \(\alpha'_k\) can be derived from the variance of the mean phenotype of the set of \(K\) partners such that
\[\mathrm{var}\left({\alpha'_k} \right) = \mathrm{var} \left( \bar{\alpha}_K \right)*n \] Conversely, we can derive the expected variance of the mean of \(n\) partners by dividing through the expected variance of a single partner \(\mathrm{var} \left( \bar{\alpha}_K \right)=\mathrm{var}\left({\alpha'_k} \right) /n\). In this simplified simulation, individuals and their social partners’ SRN parameters are characterized by the same population variances, so we can simply use the expected variance of individual SRN intercepts and slopes, i.e. \(\mathrm{var}(\boldsymbol{\alpha})=\mathrm{var}(\boldsymbol{\alpha'})\), to calculate the expected variance of the mean of \(n\) partner phenotypes. We can then use this variance to transform the vector of mean partner trait values to a standardized scale (i.e. variance = 1, z-scores), and subsequently scale these standardized values to the expected variance of a single partner using \(\mathrm{var}\left({\alpha'_k} \right)\).
#extract posteriors
<- rstan::extract(stan_results3.2)
post
#temporary vectors for assortment coefficients
= post$V_P
SRN_PV = post$sd_P
SRN_Psd = post$V_P / I_partner #expected variance for mean of partners
SRN_PVmean = sqrt(SRN_PVmean) #expected SD for mean of partners
SRN_Psdmean <- post$SRN_P[,,1] #individual intercepts
SRN_focal1 <- post$SRN_P[,,2] #individual slopes
SRN_focal2 <- cbind(post$partner_meanm[,,1], post$partner_meanf[,,1])
SRN_partner1 <- cbind(post$partner_meanm[,,2], post$partner_meanf[,,2])
SRN_partner2
#scale mean partner variance to variance of single partner
= SRN_partner1
SRN_partner1s for(j in 1:nrow(SRN_partner1))
= ( SRN_partner1[j,] / SRN_Psdmean[j,1] ) * SRN_Psd[j,1] }
{SRN_partner1s[j,]
= SRN_partner2
SRN_partner2s for(j in 1:nrow(SRN_partner2))
= ( SRN_partner2[j,] / SRN_Psdmean[j,2] ) * SRN_Psd[j,2] }
{SRN_partner2s[j,]
#assortment matrix
= list()
Beta_alpha
#generate matrices across each posterior sample
for(j in 1:nrow(SRN_focal1))
{= matrix(NA,2,2)
Beta_mat
#mu' ~ mu
1,1] = cov(SRN_focal1[j,], SRN_partner1s[j,])/var(SRN_focal1[j,])
Beta_mat[#mu' ~ psi
2,1] = cov(SRN_focal2[j,], SRN_partner1s[j,])/var(SRN_focal2[j,])
Beta_mat[#psi' ~ mu
1,2] = cov(SRN_focal1[j,], SRN_partner2s[j,])/var(SRN_focal1[j,])
Beta_mat[#psi' ~ psi
2,2] = cov(SRN_focal2[j,], SRN_partner2s[j,])/var(SRN_focal2[j,])
Beta_mat[
= Beta_mat
Beta_alpha[[j]]
}
#extract beta_psi'psi (assortment on SRN slopes)
= unlist(lapply(Beta_alpha, function(x) x[2,2]))
Beta_psi mean(Beta_psi); sum(Beta_psi > 0)/length(Beta_psi)
## [1] 0.1608
## [1] 0.989
As expected, although the estimated assortment coefficient \(\beta_{\bar{\psi'}\psi}\) is downwardly estimated from its true value 0.3, positive assortment of moderate effect size is detected.
3.4 Phenotypic model
A phenotypic between partner model can also be estimated whenever quantitative genetic information is missing.
write("
data {
//indices and scalars used for model specification
int<lower=1> N_sex; //total aggression observations per sex (I/2 * 4 lifetime partners)
int<lower=0> I; //total individuals (M + F)
int<lower=0> Im; //number of males
int<lower=0> If; //number of females
int<lower=1> idm[N_sex]; //index of male AG observations (of length N_sex)
int<lower=1> idf[N_sex]; //index of female AG observations
int<lower=1> partners_m [Im,5]; //index of male partner IDs, first column is focal ID (1 + 4 IDs)
int<lower=1> partners_f [If,5]; //index of female partner IDs, first column is focal ID (1 + 4 IDs)
//empirical data
matrix[I,I] A; //relatedness matrix
real AG_m[N_sex]; //male aggression measurements
real AG_f[N_sex]; //female aggression measurements
real time[N_sex]; //time index (=1 for all measures)
}
transformed data{
matrix[I,I] LA = cholesky_decompose(A); //lower-triangle A matrix
}
parameters {
//population effects
real alpha_0; //aggression global intercept
real psi_1; //expected interaction coefficient
real beta_B; //slope of between-partner component
//no way to partition feedback when t=1
//real<lower=-1,upper=1> phi; //(-1,1) ensures unique solution
//random effects (standard deviations)
vector<lower=0, upper = 1>[2] sd_P; //phenotypic SRN mu & psi SDs
vector<lower=0, upper = 1>[2] sd_R; //male & female residual SDs
cholesky_factor_corr[2] LP; //phenotypic SRN correlations
cholesky_factor_corr[2] LR; //sex-specific residual correlations
matrix[I,2] std_devP; //individual-level unscaled G SRN deviations
}
transformed parameters {
vector<lower=0>[2] sd_P; //SDs of P effects
matrix[I,2] SRN_P; //scaled P SRN parameter deviations
matrix[If, 2] partner_meanm; //average SRN parameters of males' partners
matrix[Im, 2] partner_meanf; //average SRN parameters of females' partners
//non-centered parameterization of phenotypic effects
SRN_P = std_devP * diag_pre_multiply(sd_P, LP)';
//calculate the mean SRN parameters of each male's lifetime partners
for(i in 1:Im) partner_meanm[i] = [mean(col(SRN_P[partners_m[i,2:5]],1)),
mean(col(SRN_P[partners_m[i,2:5]],2))];
//calculate the mean SRN parameters of each female's lifetime partners
for(i in 1:If) partner_meanf[i] = [mean(col(SRN_P[partners_f[i,2:5]],1)),
mean(col(SRN_P[partners_f[i,2:5]],2))];
}
model{
//separate male and female vectors for efficiency
matrix[Im,2] SRN_Pm = SRN_P[1:Im]; //male SRN phenotypic deviations
matrix[If,2] SRN_Pf = SRN_P[(Im+1):I]; //female SRN phenotypic deviations
//separate SRN intercepts and slopes (phenotypic deviations)
vector[Im] mu_m = col(SRN_Pm,1); //SRN intercepts
vector[If] mu_f = col(SRN_Pf,1);
vector[Im] psi_m = col(SRN_Pm,2); //SRN slopes
vector[If] psi_f = col(SRN_Pf,2);
//separate mean partner SRN intercepts and slopes
vector[Im] mu_meanm = col(partner_meanm,1); //mean partner SRN intercept for males
vector[If] mu_meanf = col(partner_meanf,1); //...for females
vector[Im] psi_meanm = col(partner_meanm,2); //mean partner SRN slope for males
vector[If] psi_meanf = col(partner_meanf,2); //...for females
//add in new vectors for within-individual centering
vector[N_sex] eta_Wm; //male SRN trait value
vector[N_sex] eta_Wf; //female SRN trait value
vector[N_sex] eta_Bm; //individual male SRN trait value toward average partner
vector[N_sex] eta_Bf; //individual female SRN trait toward average partner
//other components of linear predictor
vector[N_sex] linpred_m; //total expected value for male responses
vector[N_sex] linpred_f; //total expected value for female responses
vector[N_sex] epsilon_m; //residual for male responses
vector[N_sex] epsilon_f; //residual for female responses
//Male and female aggression response model
for (n in 1:N_sex) {
//SRN trait values (within-individual centered)
//there is no residual feedback, so only t = 1
//within-male male eta[t=1] = mu_j + (psi + psi_j)*(mu_k - mean_mu_K)
eta_Wm[n] = mu_m[idm[n]] + (psi_1 + psi_m[idm[n]])*(mu_f[idf[n]] - mu_meanm[idm[n]]) ;
//female eta[t=1] = mu_k + (psi + psi_k)*(mu_j - mean_mu_J)
eta_Wf[n] = mu_f[idf[n]] + (psi_1 + psi_f[idf[n]])*(mu_m[idm[n]] - mu_meanf[idf[n]]);
//average time=1 individual eta (used for feedback process)
//between male eta[t=1] = mu_j + psi_j*mean_mu_K
eta_Bm[n] = (psi_1 + psi_m[idm[n]])*mu_meanm[idm[n]];
//between female eta[t=1] = mu_k + psi_k*mean_mu_J
eta_Bf[n] = (psi_1 + psi_f[idf[n]])*mu_meanf[idf[n]];
//add global intercept and between-individual parameters to linear predictor
//other fixed effects can also be added here
linpred_m[n] = alpha_0 + eta_Wm[n] + beta_B*eta_Bm[n] ;
linpred_f[n] = alpha_0 + eta_Wf[n] + beta_B*eta_Bf[n] ;
//there is no residual feedback, so only t = 1
epsilon_m [n] = AG_m[n] - linpred_m[n];
epsilon_f [n] = AG_f[n] - linpred_f[n];
//correlated residuals between partners
[epsilon_m[n],epsilon_f[n]]' ~ multi_normal_cholesky([0,0], diag_pre_multiply(sd_R, LR));
}
//model priors
//fixed effects
alpha_0 ~ std_normal();
psi_1 ~ std_normal();
beta_B ~ std_normal();
//random effects
to_vector(sd_P) ~ cauchy(0,1);
to_vector(sd_R) ~ cauchy(0,1);
LP ~ lkj_corr_cholesky(2);
LR ~ lkj_corr_cholesky(2);
to_vector(std_devP) ~ std_normal();
//reaction norm heritability
to_vector(SRN_h2) ~ beta(1.2,1.2);
}
generated quantities{
//cor and cov matrices of SRN parameters and residuals
matrix[2,2] Pcor = LP * LP'; //P SRN correlation matrix
matrix[2,2] Rcor = LR * LR'; //residual correlation matrix
matrix[2,2] Pcov = diag_matrix(sd_P)*Pcor*diag_matrix(sd_P); //phenotypic covariance
matrix[2,2] Rcov = iag_matrix(sd_R)*Rcor*diag_matrix(sd_R); //residual covariance
//variances
vector<lower=0>[2] V_P = sd_P .* sd_P;
vector<lower=0>[2] V_R = sd_R .* sd_R;
}", "sam3_2p.stan")