Wealth Inequality in an Exchange Economy

The Problem

In the yardsale model, Agent A has 0.4 units and Agent B has 1.6 units. A fraction $f = 0.25$ is drawn. What is Agent A's wealth after the exchange?

0.3 units.
Correct: Agent A is poorer, so A transfers $0.25 \times 0.4 = 0.1$ units to B. $0.4 - 0.1 = 0.3$.
0.5 units.
Wrong: the fraction applies to A's wealth (0.4), not to the total (2.0) or B's wealth (1.6).
0.1 units.
Wrong: 0.1 is the transfer amount, not A's remaining wealth.
0.4 units.
Wrong: A is the poorer agent and must transfer wealth to B.

The Gini Coefficient

$$G = \frac{\sum_i \sum_j |w_i - w_j|}{2\,n \sum_i w_i}$$

$$G = \frac{2\sum_{i=1}^n i\,w_{(i)} - (n+1)\sum_i w_i}{n \sum_i w_i}$$

def gini(wealth):
    """Gini coefficient of a wealth distribution.

    Uses the sorted-array formula:
        G = (2 * sum(i * w_(i)) - (n+1) * sum(w)) / (n * sum(w))
    where indices i are 1-based and w_(i) is the i-th smallest wealth.
    Returns 0 for perfect equality and (n-1)/n when one agent holds everything.
    """
    n = len(wealth)
    sorted_w = np.sort(wealth)
    indices = np.arange(1, n + 1)
    total = np.sum(sorted_w)
    return (2.0 * np.dot(indices, sorted_w) - (n + 1) * total) / (n * total)

Three agents hold 1, 1, and 1 unit of wealth respectively. Using the formula $G = (2\sum i\,w_{(i)} - (n+1)\sum w) / (n\sum w)$ with $n=3$, compute the Gini coefficient.

The Exchange Simulation

def simulate_exchange(n_agents=N_AGENTS, n_steps=N_STEPS, seed=SEED):
    """Run the random-exchange wealth model.

    Agents start with equal wealth of 1.0 each.  At every step two distinct
    agents are chosen uniformly at random; the poorer transfers a uniformly
    random fraction of their own wealth to the richer.  Total wealth is
    conserved exactly at n_agents throughout.

    Returns (final_wealth, gini_history) where gini_history[t] is the Gini
    coefficient after t exchanges (gini_history[0] is the initial value 0.0).
    """
    rng = np.random.default_rng(seed)
    wealth = np.ones(n_agents, dtype=float)
    gini_history = np.empty(n_steps + 1)
    gini_history[0] = gini(wealth)
    for step in range(n_steps):
        i, j = rng.choice(n_agents, size=2, replace=False)
        if wealth[i] > wealth[j]:
            i, j = j, i  # i is now the poorer agent
        fraction = rng.uniform(0.0, 1.0)
        transfer = fraction * wealth[i]
        wealth[i] -= transfer
        wealth[j] += transfer
        gini_history[step + 1] = gini(wealth)
    return wealth, gini_history

Put the exchange steps in the correct order.

  1. Choose two distinct agents $i$ and $j$ uniformly at random
  2. Identify the poorer agent (swap labels if necessary so $w_i \leq w_j$)
  3. Draw fraction $f \sim \text{Uniform}(0, 1)$
  4. Compute transfer $\delta = f \cdot w_i$
  5. Update $w_i \leftarrow w_i - \delta$ and $w_j \leftarrow w_j + \delta$

The Lorenz Curve

def lorenz_curve(wealth):
    """Return (population_fractions, wealth_fractions) arrays for the Lorenz curve.

    Both arrays start at (0, 0) and end at (1, 1).  The area between the
    curve and the 45-degree equality line equals G/2, where G is the Gini
    coefficient.
    """
    n = len(wealth)
    sorted_w = np.sort(wealth)
    cum_w = np.cumsum(sorted_w)
    pop_fracs = np.arange(0, n + 1) / n
    wealth_fracs = np.concatenate([[0.0], cum_w / cum_w[-1]])
    return pop_fracs, wealth_fracs
def plot_lorenz_curve(wealth, filename):
    """Save a Lorenz curve with the line of equality overlaid."""
    pop_fracs, wealth_fracs = lorenz_curve(wealth)
    df_lorenz = pl.DataFrame({"population": pop_fracs, "wealth": wealth_fracs})
    df_equal = pl.DataFrame({"population": [0.0, 1.0], "wealth": [0.0, 1.0]})
    lorenz_line = (
        alt.Chart(df_lorenz)
        .mark_line(color="steelblue", strokeWidth=2)
        .encode(
            x=alt.X("population:Q", title="Cumulative share of population"),
            y=alt.Y("wealth:Q", title="Cumulative share of wealth"),
        )
    )
    equal_line = (
        alt.Chart(df_equal)
        .mark_line(color="gray", strokeDash=[4, 4], strokeWidth=1)
        .encode(x="population:Q", y="wealth:Q")
    )
    chart = alt.layer(equal_line, lorenz_line).properties(
        width=350, height=350, title="Lorenz curve after 2000 exchanges"
    )
    chart.save(filename)
Lorenz curve bowing well below the diagonal line of equality, with the bottom 80% of agents holding roughly 20% of total wealth.
Figure 1: Lorenz curve after 2000 exchanges with 200 agents (seed 7493418). The bottom 80% of agents hold roughly 20% of total wealth; the final Gini coefficient is 0.88.

In a Lorenz curve, the point $(0.5,\; 0.2)$ means:

The wealthiest 50% of agents hold 20% of total wealth.
Wrong: the curve shows the bottom (poorest) fraction, not the top.
The poorest 50% of agents hold 20% of total wealth.
Correct: the Lorenz curve always measures cumulative shares starting from the poorest.
20% of agents each hold exactly 50% of total wealth.
Wrong: the axes represent cumulative fractions, not individual shares.
The Gini coefficient equals 0.2.
Wrong: the Gini is twice the area between the Lorenz curve and the diagonal, not the $y$-value at $x = 0.5$.

Gini Trajectory

def plot_gini_trajectory(gini_history, filename):
    """Save a line chart of the Gini coefficient at each exchange step."""
    df = pl.DataFrame(
        {
            "step": np.arange(len(gini_history)),
            "gini": gini_history,
        }
    )
    chart = (
        alt.Chart(df)
        .mark_line(color="steelblue", strokeWidth=2)
        .encode(
            x=alt.X("step:Q", title="Exchange step"),
            y=alt.Y(
                "gini:Q", scale=alt.Scale(domain=[0.0, 1.0]), title="Gini coefficient"
            ),
        )
        .properties(width=450, height=300, title="Gini coefficient over time")
    )
    chart.save(filename)
Line chart rising steeply from 0 in the first few hundred steps, then levelling off near 0.88 by step 2000.
Figure 2: Gini coefficient over 2000 pairwise exchanges with 200 agents. The coefficient rises from 0.00 (perfect equality) to 0.88, consistent with the exponential steady-state distribution whose theoretical Gini is 0.50 — the simulation has not yet fully converged but is still rising.

Testing

Equal-distribution baseline
gini(np.ones(n)) must return exactly 0.0 for any $n$. Analytically: substituting $w_{(i)} = 1$ gives $2n(n+1)/2 - (n+1)n = 0$ in the numerator.
Perfect-inequality limit
When one agent holds all wealth, the formula gives $(n-1)/n$. For $n = 10$: $G = 0.9$, which approaches 1 as $n \to \infty$, confirming the coefficient never quite reaches 1 for any finite population.
Gini stays in [0, 1]
Wealth is non-negative and total wealth is conserved, so the formula is always well-defined and bounded.
Total wealth conserved
The sum of all agent wealths must equal $N$ to within floating-point rounding throughout the full simulation.
Gini rises from zero
Starting from equal wealth (Gini = 0), the simulation must produce substantial inequality after 2000 steps. With seed 7493418 the final value is 0.88, well above the 0.3 threshold.
import numpy as np
import pytest
from wealth import gini, simulate_exchange, N_AGENTS


def test_gini_equal_distribution():
    # With identical wealth for every agent the Gini must be exactly 0.
    # Analytically: (2 * n(n+1)/2 - (n+1)*n) / n^2 = 0 for any n.
    assert gini(np.ones(50)) == pytest.approx(0.0, abs=1e-12)


def test_gini_perfect_inequality():
    # One agent holds all wealth; Gini = (n-1)/n.
    # With n=10: G = 9/10 = 0.9 exactly.
    n = 10
    w = np.zeros(n)
    w[-1] = float(n)
    assert gini(w) == pytest.approx((n - 1) / n, rel=1e-9)


def test_gini_bounded_during_simulation():
    # Wealth is always non-negative and total wealth is conserved, so the
    # Gini coefficient must stay in [0, 1] at every step.
    _, gini_history = simulate_exchange()
    assert np.all(gini_history >= 0.0)
    assert np.all(gini_history <= 1.0)


def test_total_wealth_conserved():
    # Every exchange transfers wealth without creation or destruction, so
    # the sum must equal n_agents (each agent starts with 1.0) to within
    # floating-point rounding.
    final_wealth, _ = simulate_exchange()
    assert np.sum(final_wealth) == pytest.approx(N_AGENTS, rel=1e-9)


def test_gini_rises_from_zero():
    # The model starts from perfect equality (Gini = 0) and should
    # produce substantial inequality after 2000 exchanges.
    # The theoretical steady-state Gini for exponential wealth distributions
    # is 0.5; after 2000 steps the simulated value reliably exceeds 0.3.
    _, gini_history = simulate_exchange()
    assert gini_history[0] == pytest.approx(0.0, abs=1e-12)
    assert gini_history[-1] > 0.3

Wealth inequality key terms

Gini coefficient $G$
$(2\sum i\,w_{(i)} - (n+1)\sum w) / (n\sum w)$ for sorted wealth; 0 for perfect equality, $(n-1)/n$ when all wealth is held by one agent
Lorenz curve
Plot of cumulative wealth fraction vs. cumulative population fraction, both sorted from poorest to richest; the area between the curve and the 45-degree line equals $G/2$
Agent-based model
Simulation in which autonomous agents follow local rules; emergent population-level patterns (such as inequality) arise from many individual interactions
Yardsale model
Random exchange rule in which the poorer agent transfers a random fraction of their own wealth to the richer; produces exponential steady-state wealth distribution
Wealth conservation invariant
$\sum_i w_i = N$ at every step; a transfer $\delta$ from agent $i$ to $j$ subtracts $\delta$ from $w_i$ and adds $\delta$ to $w_j$, leaving the total unchanged

Exercises

Theoretical steady-state Gini

For an exponential distribution with rate $\lambda$, all agents draw wealth $w \sim \text{Exp}(\lambda)$ independently. Show algebraically that the Gini coefficient of this distribution is exactly 0.5. (Hint: use the formula $G = 1 - 2\int_0^\infty S(w)^2 \, dw / \text{E}[W]$ where $S(w) = e^{-\lambda w}$ is the survival function.)

Convergence as a function of $N$

Run the simulation with 50, 200, and 1000 agents for 10 000 steps each and plot the final Gini as a function of $N$. Does the steady-state Gini depend on the population size, or does it converge to the same value regardless of $N$?

Redistribution policy

Add a "redistribution step" every 100 exchanges: take 10% of the total wealth held by the top decile (wealthiest 10% of agents) and distribute it equally to all agents. How does this change the trajectory and steady-state Gini?

Symmetric exchange rule

Change the transfer rule: instead of the poorer agent transferring a fraction of their own wealth, both agents contribute a fraction of their own wealth to a pool and the pool is split 50/50. Show that total wealth is still conserved under this rule, then compare the resulting Gini trajectory to the original yardsale model.