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}$$

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)

The Exchange Simulation

i
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

i
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
i
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

i
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.
i
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

Do the math

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.

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.