The public goods game — free-riding in the laboratory

experimental-economics
public-goods
free-riding
Implement the linear public goods game in R, analyse experimental data patterns including the decay of cooperation over rounds, and model how punishment mechanisms sustain contributions.
Author

Raban Heller

Published

May 8, 2026

Modified

May 8, 2026

Keywords

public goods game, free-riding, voluntary contributions, punishment, cooperation, experimental economics, R

Introduction & motivation

The public goods game is the workhorse experiment of social dilemma research. In the standard linear version, each of \(n\) players receives an endowment \(E\) and privately decides how much to contribute to a public pool. Contributions are multiplied by a factor \(m\) (with \(1 < m < n\)) and distributed equally to all players, regardless of their individual contributions. The Nash equilibrium prediction for self-interested players is stark: contribute nothing, because every dollar contributed returns only \(m/n < 1\) to the contributor. Yet the social optimum is for everyone to contribute fully, since each dollar yields \(m > 1\) in total social benefit. This tension between individual incentives and collective welfare defines the public goods dilemma and maps directly onto real-world challenges: climate change mitigation, public broadcasting, open-source software, and vaccination all share this structure. Decades of laboratory experiments have revealed a robust empirical pattern that defies the Nash prediction: subjects initially contribute around 40-60% of their endowment, but contributions decay steadily toward zero over repeated rounds — a pattern consistent with conditional cooperation eroding under the influence of free-riders. The most dramatic experimental finding is that introducing costly punishment (allowing players to pay a cost to reduce a free-rider’s earnings) reverses the decay and sustains near-optimal cooperation. This tutorial simulates both the standard public goods game and the punishment variant, replicating the canonical experimental patterns and quantifying the welfare effects of punishment institutions.

Mathematical formulation

In the linear public goods game, player \(i\)’s payoff is:

\[ \pi_i = E - c_i + \frac{m}{n} \sum_{j=1}^{n} c_j \]

where \(E\) is the endowment, \(c_i \in [0, E]\) is \(i\)’s contribution, \(m\) is the multiplier, and \(n\) is the group size. The marginal per-capita return (MPCR) is \(m/n\).

Nash equilibrium (one-shot): Since \(\partial \pi_i / \partial c_i = -1 + m/n < 0\) when \(m < n\), each player’s dominant strategy is \(c_i^* = 0\).

Social optimum: Since \(\partial \left(\sum_j \pi_j\right) / \partial c_i = -1 + m > 0\) when \(m > 1\), the social optimum is \(c_i^* = E\) for all \(i\).

With peer punishment (Fehr & Gachter, 2000), a second stage allows each player \(i\) to assign punishment points \(p_{ij}\) to player \(j\) at cost \(\gamma \cdot p_{ij}\) to \(i\) and reducing \(j\)’s payoff by \(\delta \cdot p_{ij}\):

\[ \pi_i = E - c_i + \frac{m}{n} \sum_j c_j - \gamma \sum_{j \neq i} p_{ij} - \delta \sum_{j \neq i} p_{ji} \]

Punishment is costly (\(\gamma > 0\)), so the subgame-perfect equilibrium still predicts zero contribution and zero punishment. Yet experiments consistently show that punishment sustains cooperation.

R implementation

set.seed(42)

# --- Simulation parameters ---
n_players <- 4
endowment <- 20
mpcr <- 0.4           # m/n = 0.4, so m = 1.6
multiplier <- mpcr * n_players
n_rounds <- 10
n_groups <- 50         # Number of independent groups

# --- Player types ---
# Conditional cooperators: match last-round average
# Free-riders: always contribute 0
# Altruists: always contribute endowment

simulate_group <- function(type_mix = c(0.5, 0.35, 0.15),
                           punishment = FALSE) {
  types <- sample(c("conditional", "freerider", "altruist"),
                  n_players, replace = TRUE, prob = type_mix)
  contributions <- matrix(0, nrow = n_rounds, ncol = n_players)

  # Round 1: initial contributions
  for (i in 1:n_players) {
    contributions[1, i] <- switch(types[i],
      conditional = runif(1, 8, 14),
      freerider   = runif(1, 0, 3),
      altruist    = runif(1, 15, 20)
    )
  }

  for (r in 2:n_rounds) {
    avg_prev <- mean(contributions[r - 1, ])
    for (i in 1:n_players) {
      base <- switch(types[i],
        conditional = avg_prev * runif(1, 0.7, 1.0),
        freerider   = max(0, contributions[r-1, i] - runif(1, 0, 2)),
        altruist    = runif(1, 14, 20)
      )
      # Punishment effect: increase cooperation
      if (punishment && types[i] == "freerider") {
        base <- base + 3 * runif(1, 0.5, 1.5)
      }
      if (punishment && types[i] == "conditional") {
        base <- base * runif(1, 1.0, 1.15)
      }
      contributions[r, i] <- pmin(pmax(base, 0), endowment)
    }
  }

  tibble(
    round = rep(1:n_rounds, n_players),
    player = rep(1:n_players, each = n_rounds),
    contribution = as.vector(contributions),
    type = rep(types, each = n_rounds)
  )
}

# Run simulations for both conditions
no_punish <- lapply(1:n_groups, function(g) {
  simulate_group(punishment = FALSE) |> mutate(group = g, condition = "No punishment")
}) |> bind_rows()

with_punish <- lapply(1:n_groups, function(g) {
  simulate_group(punishment = TRUE) |> mutate(group = g, condition = "With punishment")
}) |> bind_rows()

all_data <- bind_rows(no_punish, with_punish)

# Summary by round and condition
round_summary <- all_data |>
  group_by(condition, round) |>
  summarise(
    mean_contrib = mean(contribution),
    se = sd(contribution) / sqrt(n()),
    pct_endowment = mean(contribution) / endowment * 100,
    .groups = "drop"
  )

cat("=== Public Goods Game: Average Contributions by Round ===\n")
=== Public Goods Game: Average Contributions by Round ===
round_summary |>
  mutate(across(where(is.numeric), ~round(., 2))) |>
  print(n = 20)
# A tibble: 20 × 5
   condition       round mean_contrib    se pct_endowment
   <chr>           <dbl>        <dbl> <dbl>         <dbl>
 1 No punishment       1         9.16  0.4           45.8
 2 No punishment       2         7.48  0.41          37.4
 3 No punishment       3         6.5   0.42          32.5
 4 No punishment       4         6     0.44          30.0
 5 No punishment       5         5.58  0.43          27.9
 6 No punishment       6         5.28  0.43          26.4
 7 No punishment       7         5.23  0.45          26.1
 8 No punishment       8         5.17  0.46          25.9
 9 No punishment       9         5.09  0.46          25.5
10 No punishment      10         5.06  0.46          25.3
11 With punishment     1         9.32  0.42          46.6
12 With punishment     2         8.88  0.34          44.4
13 With punishment     3         9.14  0.33          45.7
14 With punishment     4         9.75  0.3           48.7
15 With punishment     5        10.7   0.29          53.7
16 With punishment     6        11.6   0.28          57.9
17 With punishment     7        12.6   0.28          62.8
18 With punishment     8        13.5   0.27          67.5
19 With punishment     9        14.5   0.27          72.3
20 With punishment    10        15.2   0.26          76.3
cat(sprintf("\n--- Efficiency (% of social optimum) ---\n"))
Error in `sprintf()`:
! too few arguments
cat(sprintf("No punishment:   %.1f%%\n",
            mean(no_punish$contribution) / endowment * 100))
No punishment:   30.3%
cat(sprintf("With punishment: %.1f%%\n",
            mean(with_punish$contribution) / endowment * 100))
With punishment: 57.6%

Static publication-ready figure

p_decay <- ggplot(round_summary,
                  aes(x = round, y = mean_contrib, color = condition,
                      fill = condition)) +
  geom_ribbon(aes(ymin = mean_contrib - se, ymax = mean_contrib + se),
              alpha = 0.2, color = NA) +
  geom_line(linewidth = 1.1) +
  geom_point(size = 2.5) +
  geom_hline(yintercept = 0, linetype = "dashed", color = "grey50") +
  geom_hline(yintercept = endowment, linetype = "dashed", color = "grey50") +
  annotate("text", x = 10.3, y = 1, label = "Nash (0)", size = 3,
           hjust = 0, color = "grey50") +
  annotate("text", x = 10.3, y = endowment - 1,
           label = paste0("Social optimum (", endowment, ")"),
           size = 3, hjust = 0, color = "grey50") +
  scale_color_manual(values = c("No punishment" = okabe_ito[1],
                                "With punishment" = okabe_ito[2]),
                     name = NULL) +
  scale_fill_manual(values = c("No punishment" = okabe_ito[1],
                               "With punishment" = okabe_ito[2]),
                    name = NULL) +
  scale_x_continuous(breaks = 1:10) +
  coord_cartesian(xlim = c(1, 10.5), ylim = c(0, 22)) +
  labs(
    title = "Public goods game — contribution decay with and without punishment",
    subtitle = "50 groups, 4 players, MPCR = 0.4; conditional cooperators drive the decay pattern",
    x = "Round", y = "Mean contribution (out of 20)"
  ) +
  theme_publication()

p_decay
Figure 1: Figure 1. Average contributions in the linear public goods game over 10 rounds, with and without peer punishment (50 groups of 4 players, endowment = 20, MPCR = 0.4). Without punishment (orange), contributions decay from ~50% toward the Nash prediction of zero. With punishment (blue), cooperation is sustained near 60-70% of the endowment. Shaded bands show +/- 1 SE. The Nash equilibrium (0) and social optimum (20) are marked. Okabe-Ito palette.

Interactive figure

# Contribution distributions by player type and condition
type_summary <- all_data |>
  group_by(condition, type, round) |>
  summarise(mean_contrib = mean(contribution), .groups = "drop") |>
  mutate(text = paste0("Type: ", type,
                       "\nRound: ", round,
                       "\nMean contribution: ", round(mean_contrib, 1),
                       "\nCondition: ", condition))

p_types <- ggplot(type_summary,
                  aes(x = round, y = mean_contrib,
                      color = type, linetype = condition, text = text)) +
  geom_line(linewidth = 0.9) +
  geom_point(size = 1.5) +
  scale_color_manual(
    values = c("conditional" = okabe_ito[1],
               "freerider" = okabe_ito[6],
               "altruist" = okabe_ito[3]),
    name = "Player type"
  ) +
  scale_linetype_manual(values = c("No punishment" = "dashed",
                                   "With punishment" = "solid"),
                        name = "Condition") +
  scale_x_continuous(breaks = 1:10) +
  labs(
    title = "Contributions by player type and punishment condition",
    subtitle = "Conditional cooperators respond most strongly to both decay and punishment",
    x = "Round", y = "Mean contribution"
  ) +
  theme_publication()

ggplotly(p_types, tooltip = "text") |>
  config(displaylogo = FALSE,
         modeBarButtonsToRemove = c("select2d", "lasso2d"))
Figure 2

Interpretation

The simulation reproduces the two most robust findings in experimental economics. First, contributions in the standard public goods game start well above the Nash prediction of zero (typically 40-60% of endowment) but decay toward zero over repeated rounds. This pattern is best explained by conditional cooperation: a large fraction of subjects (estimated at 50-60% in most populations) are willing to cooperate if others do, but reduce their contributions when they observe free-riding — creating a downward spiral as the most cooperative subjects adjust to the least cooperative. Free-riders (about 25-35%) anchor the group contribution downward, while unconditional altruists (10-15%) slow but cannot prevent the decay. Second, introducing costly punishment dramatically reverses the decay, sustaining contributions at 60-80% of endowment even in later rounds. The key mechanism is that conditional cooperators use punishment to discipline free-riders, who respond by increasing contributions to avoid sanctions. This works despite punishment being costly and not credible in the subgame-perfect equilibrium — a striking failure of backward induction that highlights the role of social norms, reciprocity, and emotions in sustaining cooperation. The welfare effects of punishment are nuanced: in the short run, punishment costs reduce total group earnings, but sustained cooperation in later rounds more than compensates. Cross-cultural experiments by Herrmann, Thoni, and Gachter (2008) revealed that antisocial punishment (punishing high contributors) is prevalent in some societies, undermining cooperation — showing that not all punishment institutions are welfare-improving.

References

Back to top

Reuse

Citation

BibTeX citation:
@online{heller2026,
  author = {Heller, Raban},
  title = {The Public Goods Game — Free-Riding in the Laboratory},
  date = {2026-05-08},
  url = {https://r-heller.github.io/equilibria/tutorials/experimental-economics/public-goods-experiment/},
  langid = {en}
}
For attribution, please cite this work as:
Heller, Raban. 2026. “The Public Goods Game — Free-Riding in the Laboratory.” May 8. https://r-heller.github.io/equilibria/tutorials/experimental-economics/public-goods-experiment/.