run <- function(q, rule, N = 100, rounds = 500, e = 0.02, seed = 449) {
set.seed(seed)
opinion <- matrix(1L, N, N) # [i, j] = i's view of j; 1 good, 0 bad
coop <- numeric(rounds); agree <- numeric(rounds)
for (r in seq_len(rounds)) {
ord <- sample.int(N)
donors <- ord[1:(N / 2)]; recips <- ord[(N / 2 + 1):N]
helped <- 0
for (k in seq_along(donors)) {
d <- donors[k]; v <- recips[k]
act <- opinion[d, v] == 1L && runif(1) > e
helped <- helped + act
obs <- union(which(runif(N) < q), d) # the donor always knows what it did
rec_good <- opinion[cbind(obs, v)] == 1L
new <- switch(rule,
standing = as.integer(act | !rec_good),
stern = as.integer(ifelse(rec_good, act, !act)),
scoring = as.integer(rep(act, length(obs))))
opinion[cbind(obs, d)] <- new
}
coop[r] <- helped / length(donors)
p <- colMeans(opinion) # fraction of the population calling j good
agree[r] <- mean(p^2 + (1 - p)^2) # chance two random observers agree about j
}
c(cooperation = mean(tail(coop, 100)), agreement = mean(tail(agree, 100)))
}Public and private reputation
Every result in the assessment rules post came from one hidden assumption: that the population holds a single shared record. Everyone sees the same interaction, applies the same rule, and reaches the same verdict. Real populations do not work that way. You see some encounters and miss others, hear about a few second hand, and end up with your own private opinion of each individual.
That change does more than add noise. It reverses the ranking of the rules. This post builds a private-information reputation model in base R and watches what happens to three of them.
The model
Track an N by N opinion matrix: entry [i, j] is individual i’s opinion of individual j, good or bad. Each round, pair everyone up, half as donors and half as recipients. A donor helps if its own opinion of the recipient is good, subject to an implementation error. Then each observer who happened to see the interaction, a fraction q of the population, updates its opinion of the donor by its assessment rule, using its own opinion of the recipient. There is no shared ledger anywhere.
The subtlety is which recipient reputation an observer uses. Under public information there is only one. Under private information every observer consults its own column of the matrix, so two observers watching the same act can reach opposite verdicts.
The agreement measure is worth explaining. For each target j, p is the share of the population that calls it good, so the chance two randomly chosen observers give the same verdict is p^2 + (1-p)^2. Averaged over targets, that is one when opinion is unanimous and one half when it is a coin flip. It is the direct measurement of how public the reputation actually is.
Running the three rules
Set q to one and every individual observes every interaction, which reproduces public information. Then lower it.
grid <- expand.grid(q = c(1, 0.5, 0.2), rule = c("standing", "stern", "scoring"),
stringsAsFactors = FALSE)
out <- t(mapply(run, grid$q, grid$rule))
res <- cbind(grid, round(out, 3))
res[order(res$rule, -res$q), ] q rule cooperation agreement
7 1.0 scoring 0.000 1.000
8 0.5 scoring 0.236 0.770
9 0.2 scoring 0.449 0.566
1 1.0 standing 0.950 1.000
2 0.5 standing 0.931 0.929
3 0.2 standing 0.869 0.828
4 1.0 stern 0.950 1.000
5 0.5 stern 0.495 0.505
6 0.2 stern 0.491 0.505
At q = 1 the model recovers the analytic picture: standing and stern judging both cooperate 95 per cent of the time with agreement exactly 1.000, and image scoring sits at zero, just as the equilibrium calculation said. That agreement of 1.000 is the check that the simulation is doing what it claims; with full observation everyone really does hold the same opinion.
Lower q and the three rules go three different ways.
Standing barely notices. Cooperation falls from 0.950 to 0.931 at half observation and 0.869 at one fifth, with agreement sliding gently to 0.929 and 0.828. The rule is forgiving in a specific way: an observer who has not seen the recipient misbehave still records the donor as good whenever the donor helped, and helping is the common case. Disagreements do not compound.
Stern judging falls apart. It matches standing at full observation and then drops to 0.495 at q = 0.5, no better than at q = 0.2. Agreement collapses to 0.505, which is chance: the population has no shared view of anyone at all. Stern judging demands that a donor refuse a bad recipient, so an observer who thinks the recipient was good and a donor who thinks it was bad will disagree about the donor’s action. Every such disagreement creates two more, and the opinion matrix shakes itself to noise. The rule that tied for best under public information is the one that cannot survive without it.
Image scoring gets better. This is the perverse result. Under full observation it collapses to zero, exactly as the deterministic model predicted. Cut observation to a half and cooperation rises to 0.236; cut it to a fifth and it reaches 0.449. Ignorance helps, because image scoring’s problem is that bad reputations propagate, and an observer who misses an interaction cannot propagate anything. Fewer eyes mean fewer chances to record a justified refusal as a defection.
library(ggplot2)
qs <- c(0.1, 0.2, 0.35, 0.5, 0.7, 1)
sweep_grid <- expand.grid(q = qs, rule = c("standing", "stern", "scoring"),
stringsAsFactors = FALSE)
sweep_out <- t(mapply(run, sweep_grid$q, sweep_grid$rule))
sweep_grid$cooperation <- sweep_out[, 1]
sweep_grid$agreement <- sweep_out[, 2]
label <- c(standing = "standing", stern = "stern judging", scoring = "image scoring")
sweep_grid$name <- label[sweep_grid$rule]
ggplot(sweep_grid, aes(q, cooperation, colour = name)) +
geom_line(linewidth = 0.9) + geom_point(size = 1.5) +
scale_colour_manual(values = c(standing = "#275139", `stern judging` = "#b5534e",
`image scoring` = "#c9b458")) +
labs(x = "Probability of observing an interaction (q)",
y = "Cooperation rate", colour = NULL) +
theme_minimal(base_size = 12) + theme(legend.position = "top")
Agreement is the mechanism
Plotting agreement alongside cooperation shows that the two are not independent quantities but the same story told twice.
ggplot(sweep_grid, aes(q, agreement, colour = name)) +
geom_hline(yintercept = 0.5, linetype = "dashed", colour = "#8d8d80") +
geom_line(linewidth = 0.9) + geom_point(size = 1.5) +
scale_colour_manual(values = c(standing = "#275139", `stern judging` = "#b5534e",
`image scoring` = "#c9b458")) +
coord_cartesian(ylim = c(0.45, 1)) +
labs(x = "Probability of observing an interaction (q)",
y = "Agreement between observers", colour = NULL) +
theme_minimal(base_size = 12) + theme(legend.position = "top")
Agreement near chance means the reputation system has stopped existing in any useful sense, even though every individual is still diligently keeping score. That is stern judging’s failure at low observation, and it explains the cooperation figure exactly.
Agreement is necessary rather than sufficient, though, and image scoring shows why. Its agreement climbs back to 1.000 at full observation, which sounds healthy until you ask what the population agrees about: everyone is bad. Consensus on a worthless verdict buys nothing. Standing is the only one of the three that holds both a shared view and a favourable one. Hilbe and colleagues (2018) reached the same conclusion analytically: private assessment breaks the leading eight, and the rules that come through it are the ones tolerant of disagreement rather than the ones that are strictest when everybody agrees.
An honest limit
The simulation has no evolution in it. Every individual is a discriminator using the same rule, so the results measure whether a rule can maintain accurate reputations, not whether it beats alternatives under selection. Adding strategy competition to the private-information model changes the numbers and is a much heavier computation.
Observation is also modelled as independent coin flips: each individual sees each interaction with the same probability, with no structure. Real populations are not like that. Individuals watch their social neighbours far more than strangers, which means observation is correlated and clusters of people share views. Correlated observation should sit somewhere between the public and private cases, but where it lands is not something this model can tell you.
Finally, the seed is fixed at 449 and only one run is reported per cell. A single run of an agent-based model is one draw. The next post puts these results through the checks that a single draw does not survive on its own.
References
Hilbe C, Schmid L, Tkadlec J, Chatterjee K, Nowak MA 2018. Proceedings of the National Academy of Sciences 115(48):12241-12246 (10.1073/pnas.1810565115)
Ohtsuki H, Iwasa Y 2006. Journal of Theoretical Biology 239(4):435-444 (10.1016/j.jtbi.2005.08.008)
Nowak MA, Sigmund K 2005. Nature 437(7063):1291-1298 (10.1038/nature04131)
Milinski M, Semmann D, Krambeck HJ 2002. Nature 415(6870):424-426 (10.1038/415424a)