How wealth inequality naturally spreads in an unconstrained market

This notebook shows how wealth distributes in an open market where a set of agents trades without any mechanisms in place to ensure a situation of extreme inequality. The system is modelled in the following way:

  • A fixed number of agents is initialized either with the same amount or a random amount of wealth within a certain range.

  • At each iteration two agents will be randomly picked up and will exchange a random amount.

  • The maximum amount exchanged will be a fraction $\Delta w$ of the poorest agent's available wealth.

  • No negative wealth is allowed. If a set of agents reaches a wealth of zero then they will no longer be involved in outgoing transactions until they receive a positive amount again.

This approach is based on the Affine Wealth Model paper published by B. Boghosian et al. in 2016.

System parameters

In [1]:
# Number of agents in the market
n_agents = 1000

# Number of iterations/transfers
n_iterations = 250_000

# Range of initial amounts
amount_range = [75, 125]

# Max fraction of the poorest's agent amount that will be involved in a transfer
max_exchanged_share = 0.25

Imports

In [2]:
import random
from dataclasses import dataclass

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from IPython.display import HTML

Class definitions

In [3]:
@dataclass
class Agent:
    """
    Models an agent in a free market.
    An agent has a unique ID and a certain amount of money at time t
    """

    id: int
    amount: float = 0.0

    def __str__(self):
        return f'[Agent {self.id} [amount={self.amount}]]'


class Market:
    """
    Models the market.

    :param n_agents: Number of agents in the system
    :param amount_range: Numerical range for the agents' initial amount. It should
        be a sorted list with only two numerical elements
    :param init_random_amount: If True then the initial amount of the agents will be
        randomly picked up within amount_range. Otherwise, all the agents will start
        with the same amount, calculated as the mean value of amount_range
    :param max_exchanged_share: Maximum fraction of the poorest agent's wealth that
        will be exchanged
    :param wealth_tax: Wealth tax, paid on each transaction if the paying part has a
        wealth greater than the average. Note that it's applied to the
        `agent.wealth - mean_wealth` difference.
    :param wealth_tax_reference: If set to `mean` then the amount after applying the
        wealth tax will be calculated as `tax * (w[A] - mean(w)`. Otherwise, if
        `median`, it'll use the median wealth rather than the average.
    """
    def __init__(
        self,
        n_agents: int,
        amount_range: list = amount_range,
        init_random_amount: bool = False,
        max_exchanged_share: float = max_exchanged_share,
        wealth_tax: float = 0.0,
        wealth_tax_reference: str = 'mean',
    ):
        self.agents = []
        self.max_exchanged_share = max_exchanged_share
        self.wealth_tax = wealth_tax
        self.wealth_aggreg = getattr(np, wealth_tax_reference)

        for i in range(0, n_agents):
            amount = (
                float(random.randint(*amount_range))
                if init_random_amount
                else amount_range[0] + ((amount_range[1] - amount_range[0]) / 2)
            )

            agent = Agent(id=i, amount=amount)
            self.agents.append(agent)

    def transact(self, a: Agent, b: Agent):
        """
        Perform a money transfer between agent A and B. Money direction: A -> B.

        :param a: Agent A
        :param b: Agent B
        """
        # Percentage of the amount owned by A that is transacted with B, randomized
        transacted_share = random.randint(0, int(self.max_exchanged_share * 100)) / 100

        # Note a detail here: the amount transacted from A to B is always a fraction
        # of the minimum total owned amount between A and B. This is to model the fact
        # that it's extremely unlikely for someone to be given in a single transaction
        # more than the total amount of wealth that they own.
        amount = min(a.amount, b.amount) * transacted_share

        # Wealth tax correction if wealth_tax > 0 and A owns more wealth than the average/median.
        # Note that in this implementation it's calculated as a share of the difference
        # between the mean wealth and A's wealth and it doesn't depend on the transacted
        # amount
        mean_w = self.wealth_aggreg([x.amount for x in self.agents])
        tax = self.wealth_tax * max(0, (a.amount - mean_w))
        amount += tax

        # Perform the transfer
        a.amount -= amount
        b.amount += amount

    def pick_pair(self):
        """
        Randomly pick a pair of agents to engage in a transfer.
        The only constraint is that an agent should have at least
        1 unit of currency on their account - this prevents
        accounts from going bankrupt, but it's a constraint that
        can be removed.
        """
        while True:
            agents = self.agents.copy()
            sender = agents.pop(random.randint(0, len(agents) - 1))
            receiver = agents.pop(random.randint(0, len(agents) - 1))

            if sender.amount >= 1:
                break

        return sender, receiver

Simulation class with animations

In [4]:
class Simulation:
    """
    A class to model a market simulation and plot the timeline.

    :param market: Market to simulate
    :param n_iterations: Number of transactions to run
    """

    def __init__(self, market: Market, n_iterations: int):
        self.animation_data = []  # Store histogram data at regular intervals
        self.n_iterations = n_iterations
        self.interval = max(1, int(self.n_iterations // 100))  # Capture ~100 frames
        self.market = market

    def run(self):
        """
        Run the simulation.
        """

        for i in range(int(self.n_iterations)):
            # Pick a random pair of agents to run a transaction
            a, b = self.market.pick_pair()
            self.market.transact(a, b)
    
            # Store data at regular intervals
            if i % self.interval == 0:
                amounts = [agent.amount for agent in self.market.agents]
                self.animation_data.append(amounts)

        return self.show()

    def show(self):
        """
        Show an animation with how the wealth distribution changes over time.
        It requires the simulation to `run` first.
        """

        # Create the animation
        fig, ax = plt.subplots(figsize=(10, 6))

        def animate(frame):
            ax.clear()
            ax.hist(self.animation_data[frame], bins=30, alpha=0.7, color='skyblue', edgecolor='black')
            ax.set_title(
                f'Wealth Distribution (wealth tax: {(self.market.wealth_tax * 100):.0f}%) '
                f'- Iteration {frame * self.interval}'
            )
            ax.set_xlabel('Wealth Buckets ($)')
            ax.set_ylabel('Number of Agents')
    
            # Add statistics
            mean_wealth = self.market.wealth_aggreg(self.animation_data[frame])
            ax.axvline(
                mean_wealth,
                color='red',
                linestyle='--',
                label=f'{"Avg" if self.market.wealth_aggreg == np.mean else "Median"}: {mean_wealth:.2f}',
            )
            ax.legend()

        # Create and display animation in notebook
        anim = animation.FuncAnimation(
            fig, animate, frames=len(self.animation_data), interval=100, blit=False, repeat=True
        )

        plt.close(fig)

        # Display the animation directly in the notebook
        return HTML(anim.to_jshtml())

Market initialization

Initialize the market and plot the initial wealth distribution.

Set init_random_amount=True to initialize the wealth of each agent with a random amount in [75, 125], False to initialize everyone's wealth to 100.

Market simulation without wealth tax

Simulate a number of random transactions without a wealth tax and show the final wealth distribution

In [5]:
market = Market(n_agents)
Simulation(market, n_iterations).run()
Out[5]:

It can be easily seen that wealth quickly stashes up in the pockets of a very small group of agents, while most of the other agents end up piling up in the lowest bucket.

The situation is the same even if we initialize the agents with random wealth instead of $100 each.

In [6]:
market = Market(n_agents, init_random_amount=True)
Simulation(market, n_iterations).run()
Out[6]:

But why?

If transactions are random, how come wealth concentrates in such a drastic fashion?

Think for a moment of being at a casino where you play a certain initial amount (let’s say $100). On each hand a coin flip will occur: if the coin lands on heads then you win back 20% of the initial amount you played, otherwise you lose 17% of the amount you played.

At first sight, it seems like a quite advantageous game: assuming that you’ve got a fair coin, on each transaction you’ll have:

$$ 0.50 \times $20 - 0.50 \times $17 = $1.50 $$

So the net balance is positive.

What happens if, however, the reasoning becomes more like “what if I stay at the table for 10 games?”. Then, assuming that half the times you win and half the times you lose, your net gain will be:

$$ (1.2)^5 \times (0.83)^5 \times \$100 = \$98.02 $$

Therefore, we played \$100, but we’re statistically likely to get back \\$98.02 after 10 games — therefore we’re playing a game with a statistical a net loss. You can apply the same principle to calculate your final gain if you stay at the table for 100 games (it’ll be \$81.84), or 1000 games (it’ll be \\$13.45).

Open markets without wealth redistribution are similar to this casino example: you win some and you lose some, but the longer you stay in the casino, the more likely you are to lose. The difference is that a free market is a casino where players can never leave.

Market simulation with wealth tax

Run the simulation again, but this time apply a wealth tax $0 \leq \chi \leq 1$ to each transaction.

Given two agents $A$ (sender) and $B$ (receiver), with respective total wealth $w_A$ and $w_B$, that transact an amount $x$, the wealth tax correction applied to $x$ will be:

$$ \begin{equation} x \leftarrow \begin{cases} x \mbox{ if } w_A \lt \bar{w} \\ x + \chi(w_A - \bar{w}) \mbox{ if } w_A \geq \bar{w} \end{cases} \end{equation} $$

Where $\bar{w}$ is the average wealth owned by all the agents.

Note that the wealth tax is a share of the difference between the total wealth owned by A and the average wealth, and it's not proportional to the transacted amount.

This helps model a fairer redistribution system.

Let's first run a simulation with an aggressive 25% wealth tax on each transaction made by a rich.

In [7]:
market = Market(n_agents, wealth_tax=0.25)
Simulation(market, n_iterations).run()
Out[7]:

That's better from the perspective of wealth equality, and also very stable over time, but now the distribution is perhaps too static to be realistic. After 250k iterations the richest one will still have at most 3-4x of their initial amount, and, since the mean wealth is quite static and variance is quite narrow under this scenario, they're very unlikely to make more money.

This may seem an acceptable scenario for some socialists, but a liberal may argue that it doesn't provide sufficient financial incentives for the most brilliant to invest in their skills.

Let's try and adjust the wealth tax to a more modest 5% instead:

In [8]:
market = Market(n_agents, wealth_tax=0.05)
Simulation(market, n_iterations).run()
Out[8]:
In [9]:
market = Market(n_agents, wealth_tax=0.01)
Simulation(market, n_iterations).run()
Out[9]:

What if we try again with a 1% wealth tax, but this time peg it to the median wealth instead of the average.

In [10]:
market = Market(
    n_agents,
    wealth_tax=0.01,
    wealth_tax_reference='median',
)

Simulation(market, n_iterations).run()
Out[10]:

Let's run a few more simulations with the median instead of the average to see if it affects the final distribution.

Wealth tax = 25% and pegged t

In [11]:
market = Market(
    n_agents,
    wealth_tax=0.25,
    wealth_tax_reference='median',
)

Simulation(market, n_iterations).run()
Out[11]:

Limitations

  1. The market modelled in this code acts like a closed system. The total available wealth doesn't increase and doesn't decrease, but it's only transferred from one agent to another. In a real world scenario you may want to model institutions like central banks that can randomly increase the total supply of available money (and probably model a distribution mechanism accordingly).
  2. A follow-up may involve a system modelled with multiple Market objects to model a more realistic scenario where countries or regions trade with one another. In such a system users between two markets can randomly interact with each other, with a probability that can be tuned it (call it "market openness").
  3. Transactions in this system are purely monetary. It doesn't take into account different services or goods that can be purchased, nor the fact that agents can also be workers with a fixed periodic injection of income, which may randomly change over time with another tunable rate - call it "social mobility".
  4. In this system any account must always have an amount > 1 in order to participate in a transaction, and it can never spend more than it owns. In a more realistic system you may also want to model debt - but this still means that the conclusions of this system are an upper bound for a more realistic one.
  5. Since transacting agents are randomly picked at each stage, you may well have a case where a billionaire randomly transfers an amount to someone who's unemployed. We all know that this case is quite unlikely. But still, this makes the conclusion of this analysis a plausible upper bound.

Conclusions

  1. Without a system of wealth redistribution a closed economic system is doomed to extreme wealth inequality regardless of its initial conditions.
  2. A wealth tax calculated as a fixed share of $(w_A - \bar{w})$ if $w_A \gt \bar{w}$, where $w_A$ is the wealth owned by agent $A$ and $\bar{w}$ is the average wealth, can drastically improve things, by keeping average wealth in check and preventing inequality even on the long run.
  3. You can end up with different wealth distribution models by tuning that tax. All the way from a purely egalitarian scenario to just a fine tune of the laissez-faire scenario. But what's important is that, no matter how small, the presence of this tax always prevents runaway wealth accumulation, even after a high number of iterations, as it has the property of always moving the distribution around its average value, and higher values will simply narrow down the variance of the wealth distribution. Which means that by tweaking your tax rate you can almost deterministically (at least in a closed system) calculate the highest spendable wealth of the richest person in your system.
  4. Replacing $\bar{w}$ with the median wealth rather than the mean didn't seem to drastically impact the final distribution.