Association indices for social networks

R
social networks
animal behaviour
association index
ecology tutorial
Turn group sightings into a weighted animal social network in R: the simple ratio and half-weight association indices by hand, and when the choice matters.
Author

Tidy Ecology

Published

2026-07-18

You watch a marked population: a flock, a pod, a troop. Each time you go out you record who was seen together. Out of those sightings you want a network: a weight on every pair of individuals that says how strongly they associate. This first post builds that network from group sightings and shows why the arithmetic you pick to do it is a real decision, not a formality.

The gambit of the group

Field data rarely record “A groomed B” or “A followed B”. They record groups: a set of individuals seen together at one time. The usual move is to treat everyone in a group as associating with everyone else in it. That assumption has a name, the gambit of the group, and the whole network inherits it. It is a strong assumption and we return to it in the checking post; for now we take it and see how far it gets us.

So the raw material is a list of observations, each one an individual seen in a group at a sampling period. A period can hold more than one group (you might see two subgroups on the same morning). Here is a small hand-built example with six individuals across fifteen periods.

obs <- data.frame(
  period = c(1,1, 2,2, 3,3, 4,4,4, 5,5, 6,6, 7, 8,8, 9,9,9, 10,10,
             11,11, 12, 13,13, 14,14, 15),
  group  = c(1,1, 1,1, 1,1, 1,1,2, 1,1, 1,1, 1, 1,1, 1,1,2, 1,1,
             1,1, 1, 1,1, 1,1, 1),
  id     = c("A","B", "A","B", "A","B", "A","B","C", "A","C", "B","C", "A",
             "C","D", "A","B","C", "D","E", "D","E", "E", "E","F", "E","F","F")
)
ids <- sort(unique(obs$id))
n <- length(ids)

Four counts per pair

For any pair of individuals the association index is a ratio, and the ratio needs four counts. Over the sampling periods, count how many times the pair was seen together, how many times both were seen but in different groups, and how many times only one of them was seen at all.

Writing the pair as \(i\) and \(j\):

  • \(x\): periods where \(i\) and \(j\) were in the same group.
  • \(y_{ab}\): periods where both were seen but in different groups.
  • \(y_a\): periods where only \(i\) was seen.
  • \(y_b\): periods where only \(j\) was seen.

The following function walks the periods and returns those four numbers for a pair.

counts <- function(i, j) {
  x <- yab <- ya <- yb <- 0L
  for (p in unique(obs$period)) {
    sub <- obs[obs$period == p, ]
    gi <- sub$group[sub$id == i]
    gj <- sub$group[sub$id == j]
    seen_i <- length(gi) > 0
    seen_j <- length(gj) > 0
    if (seen_i && seen_j) {
      if (any(gi %in% gj)) x <- x + 1L else yab <- yab + 1L
    } else if (seen_i) ya <- ya + 1L
    else if (seen_j) yb <- yb + 1L
  }
  c(x = x, yab = yab, ya = ya, yb = yb)
}
counts("A", "B")
  x yab  ya  yb 
  5   0   2   1 

Two indices, one denominator apart

The simple ratio index (SRI) puts the together-count over every period where at least one of the pair was seen:

\[\text{SRI} = \frac{x}{x + y_{ab} + y_a + y_b}.\]

The half-weight index (HWI) keeps the same numerator but halves the weight on the periods where only one member was identified:

\[\text{HWI} = \frac{x}{x + y_{ab} + \tfrac{1}{2}(y_a + y_b)}.\]

The half-weight was built for a specific worry (Cairns and Schwager 1987): in many protocols, when two animals are together you are less likely to identify both of them than when they are apart. That bias inflates the single-identification counts \(y_a\) and \(y_b\), which drags the SRI down. Halving those counts pushes back against the bias. The two indices therefore diverge exactly where \(y_a + y_b\) is large relative to \(x\): pairs often seen singly.

Let me compute both for every pair and store the two matrices.

sri <- hwi <- matrix(0, n, n, dimnames = list(ids, ids))
tab <- list()
for (a in 1:(n-1)) for (b in (a+1):n) {
  cc <- counts(ids[a], ids[b])
  s <- cc["x"] / (cc["x"] + cc["yab"] + cc["ya"] + cc["yb"])
  h <- cc["x"] / (cc["x"] + cc["yab"] + 0.5*(cc["ya"] + cc["yb"]))
  sri[ids[a], ids[b]] <- sri[ids[b], ids[a]] <- s
  hwi[ids[a], ids[b]] <- hwi[ids[b], ids[a]] <- h
  tab[[length(tab)+1]] <- data.frame(i = ids[a], j = ids[b],
      x = cc["x"], yab = cc["yab"], ya = cc["ya"], yb = cc["yb"],
      SRI = round(s, 3), HWI = round(h, 3), diff = round(h - s, 3))
}
tab <- do.call(rbind, tab); rownames(tab) <- NULL
tab[order(-tab$SRI), ]
   i j x yab ya yb   SRI   HWI  diff
1  A B 5   0  2  1 0.625 0.769 0.144
13 D E 2   0  1  3 0.333 0.500 0.167
15 E F 2   0  3  1 0.333 0.500 0.167
10 C D 1   0  4  2 0.143 0.250 0.107
6  B C 1   2  3  2 0.125 0.182 0.057
2  A C 1   2  4  2 0.111 0.167 0.056
3  A D 0   0  7  3 0.000 0.000 0.000
4  A E 0   0  7  5 0.000 0.000 0.000
5  A F 0   0  7  3 0.000 0.000 0.000
7  B D 0   0  6  3 0.000 0.000 0.000
8  B E 0   0  6  5 0.000 0.000 0.000
9  B F 0   0  6  3 0.000 0.000 0.000
11 C E 0   0  5  5 0.000 0.000 0.000
12 C F 0   0  5  3 0.000 0.000 0.000
14 D F 0   0  3  3 0.000 0.000 0.000

Read the table top to bottom. A and B were together five times (\(x = 5\)) and never seen singly enough to pull them apart, giving SRI 0.625 and HWI 0.769. Down among the weaker pairs, D and E sit together only twice but are each seen alone several times, so the two indices split further apart. The C to D edge is a single shared group: a thin bridge between what look like two clusters.

The association matrix makes the clusters visible.

long <- expand.grid(i = ids, j = ids)
long$sri <- mapply(function(a, b) sri[a, b], long$i, long$j)
ggplot(long, aes(i, j, fill = sri)) +
  geom_tile(colour = "white", linewidth = 0.6) +
  scale_fill_gradient(low = "#f5f5f5", high = "#2b5d34", name = "SRI") +
  coord_equal() +
  labs(x = NULL, y = NULL,
       title = "Pairwise association (simple ratio index)")
A six by six heatmap of association indices. The top-left three by three block and the bottom-right three by three block are darker, indicating stronger within-cluster association, with a single faint cell linking the two blocks.
Figure 1: Simple ratio index for every pair. Two blocks stand out, A to C and D to F, joined only by a weak C to D edge.

Where the choice bites

For the tight pairs the two indices tell the same story; the ranking of edges barely moves. The gap opens for pairs seen singly a lot, and that gap is not tiny.

ggplot(tab, aes(SRI, HWI)) +
  geom_abline(slope = 1, intercept = 0, linetype = "dashed", colour = "grey60") +
  geom_point(aes(size = ya + yb), colour = "#2b5d34", alpha = 0.8) +
  scale_size_continuous(name = "seen singly\n(ya + yb)", range = c(2, 6)) +
  coord_equal(xlim = c(0, 0.85), ylim = c(0, 0.85)) +
  labs(title = "The two indices diverge for pairs seen apart",
       x = "simple ratio index", y = "half-weight index")
A scatter plot with the simple ratio index on the x axis and the half-weight index on the y axis, both from zero to one. A dashed identity line runs diagonally. Points lie on or above the line, with the largest vertical gaps for pairs at low association values.
Figure 2: Half-weight against simple ratio for every pair. Points on the dashed line would mean the indices agree; the pairs seen singly most often sit furthest above it.
c(max_HWI_minus_SRI = round(max(abs(tab$diff)), 3),
  edges_present = sum(sri[upper.tri(sri)] > 0),
  dyads_total = n * (n - 1) / 2)
max_HWI_minus_SRI     edges_present       dyads_total 
            0.167             6.000            15.000 

The largest gap between the indices for this dataset is 0.167, and it lands on the pairs with the most single sightings. On a scale from zero to one that is enough to reorder a middle-ranked edge or to move a pair above or below whatever threshold you later use to draw the network. The indices agree at the top and disagree in the middle, which is precisely the range where marginal associations get decided.

What this settles and what it does not

The practical rule is short. Use the simple ratio index when you are confident that seeing one animal without its partner really means they were apart, so the single-identification counts are trustworthy. Reach for the half-weight index when your protocol tends to miss the second animal of a pair, so those counts are inflated. Whichever you choose, report it: an SRI network and an HWI network from the same sightings are not interchangeable at the edges.

Two limits are worth stating now. First, both indices rest on the gambit of the group, and a weight of 0.625 between A and B means “seen in the same group five times out of eight”, not “socially bonded”. Second, the whole thing depends on how a group was defined in the field, and that definition is a decision you made, not a fact the animals handed you. The checking post takes both of these apart.

With the network in hand, the next question is whether any of its structure is real or whether it is what random mixing would produce anyway. That needs a null model, and for social networks the choice of null is the difference between a sound result and a confident mistake.

Where to go next

Build the network first, then interrogate it. The permutation post shows how to ask whether the associations you measured are more than chance, and why the obvious shuffle gives the wrong answer. The centrality post turns the matrix into per-individual scores, and the checking post returns to the two assumptions flagged above.

References

Cairns SJ, Schwager SJ 1987 Animal Behaviour 35(5):1454-1469 (10.1016/S0003-3472(87)80018-0)

Whitehead H 2008 Analyzing Animal Societies: Quantitative Methods for Vertebrate Social Analysis. University of Chicago Press. ISBN 978-0226895215

Farine DR, Whitehead H 2015 Journal of Animal Ecology 84(5):1144-1163 (10.1111/1365-2656.12418)

Newsletter

Get new tutorials by email

New R and QGIS tutorials for ecologists, straight to your inbox. No spam; unsubscribe anytime.

By subscribing you agree to receive these emails and confirm your address once. See the privacy policy.