make_pop <- function(p, r, G, n) { # G groups of n, relatedness r
s <- sqrt(r); founder <- rbinom(G, 1, p)
g <- as.vector(sapply(founder, function(f) ifelse(runif(n) < s, f, rbinom(n, 1, p))))
list(g = g, grp = rep(seq_len(G), each = n))
}
selection_on_allele <- function(b, c = 1, r = 0.5, p = 0.3, G = 12000, n = 4, syn = 0, seed = 2) {
set.seed(seed); po <- make_pop(p, r, G, n); g <- po$g; grp <- po$grp
g_others <- (ave(g, grp, FUN = sum) - g) / (n - 1) # mean genotype of the OTHER groupmates
w <- 1 - c * g + b * g_others + syn * g * g_others # fitness: pay c, receive b from group
mean((w - mean(w)) * (g - mean(g))) / mean(w) # Cov(w, g) / wbar
}Hamilton’s rule and kin selection
Why would an animal ever help another at a cost to itself? Hamilton (1964) gave the answer that founded social evolution: a helping allele can spread if the help falls often enough on others who carry it. His rule is one line,
\[ r b > c, \]
where c is the fitness cost to the altruist, b the benefit to the recipient, and r their genetic relatedness. This post builds an altruism allele in a structured population in base R and shows that it spreads exactly when rb beats c, not before.
An altruism allele in family groups
Take a large population split into groups of relatives. Each individual carries an altruism allele (1) or not (0), and within a group the genotypes are correlated with relatedness r. An altruist pays a cost c and spreads a benefit b across its groupmates. Whether the allele is favoured is a selection differential: the covariance of fitness with genotype, straight from the Price equation.
The rule in action
Fix relatedness at r = 0.5 (full siblings) and cost at c = 1. Hamilton’s rule says the allele spreads once b exceeds c / r = 2. Sweep the benefit across that threshold.
r <- 0.5; c <- 1
cat(sprintf("Hamilton threshold: b* = c/r = %.2f\n", c / r))Hamilton threshold: b* = c/r = 2.00
for (b in c(1.0, 2.0, 3.0))
cat(sprintf("b = %.1f (rb - c = %+.2f): selection on allele = %+.4f\n",
b, r * b - c, selection_on_allele(b)))b = 1.0 (rb - c = -0.50): selection on allele = -0.1073
b = 2.0 (rb - c = +0.00): selection on allele = -0.0025
b = 3.0 (rb - c = +0.50): selection on allele = +0.0627
Below the threshold, at b = 1, selection on the allele is negative (-0.107): altruism is weeded out. At the threshold b = 2 it is essentially zero (-0.003), the knife-edge where rb = c. Above it, at b = 3, selection turns positive (+0.063) and altruism spreads. The sign of selection tracks rb - c exactly, because that quantity is the covariance of fitness with genotype once relatedness is folded in.
library(ggplot2)
bs <- seq(0.5, 3.5, 0.25)
dd <- data.frame(b = bs, sel = vapply(bs, selection_on_allele, numeric(1)))
ggplot(dd, aes(b, sel)) +
geom_hline(yintercept = 0, colour = "#93a87f", linewidth = 0.4) +
geom_vline(xintercept = c / r, linetype = "dashed", colour = "#93a87f") +
geom_line(colour = "#275139", linewidth = 0.9) + geom_point(colour = "#16241d", size = 1.6) +
annotate("text", x = c / r + 0.05, y = -0.08, label = "b* = c/r", colour = "#46604a", size = 3.4, hjust = 0) +
labs(x = "Benefit b", y = "Selection on altruism allele") +
theme_minimal(base_size = 12)
An honest limit
Hamilton’s rule in this clean form assumes the fitness effects are additive: the cost and benefit simply add up, with no synergy between cooperating partners. When helping pays off only if a partner also helps, or when benefits saturate, rb > c can predict the wrong direction. It also assumes the benefit lands on relatives rather than on competitors, which is not automatic in a crowded patch. Both assumptions fail in ways that matter, and the checking post works through them. The rule is the right first question to ask about any cooperative behaviour; it is not the last word once payoffs stop being additive.