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.
# 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
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
@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
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())
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.
Simulate a number of random transactions without a wealth tax and show the final wealth distribution
market = Market(n_agents)
Simulation(market, n_iterations).run()
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.
market = Market(n_agents, init_random_amount=True)
Simulation(market, n_iterations).run()
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.
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.
market = Market(n_agents, wealth_tax=0.25)
Simulation(market, n_iterations).run()
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:
market = Market(n_agents, wealth_tax=0.05)
Simulation(market, n_iterations).run()
market = Market(n_agents, wealth_tax=0.01)
Simulation(market, n_iterations).run()
What if we try again with a 1% wealth tax, but this time peg it to the median wealth instead of the average.
market = Market(
n_agents,
wealth_tax=0.01,
wealth_tax_reference='median',
)
Simulation(market, n_iterations).run()
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
market = Market(
n_agents,
wealth_tax=0.25,
wealth_tax_reference='median',
)
Simulation(market, n_iterations).run()
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").