Wealth distribution model
This model is a simple agent-based economy that is modelled according to the work of Dragulescu et al.. This work introduces statistical mechanics concepts to study wealth distributions. What we show here is also referred to as "Boltzmann wealth distribution" model.
This model has a version with and without space. The rules of the space-less game are quite simple:
- There is a pre-determined number of agents.
- All agents start with one unit of wealth.
- At every step an agent gives 1 unit of wealth (if they have it) to some other agent.
Even though this rule-set is simple, it can still recreate the basic properties of wealth distributions, e.g. power-laws distributions.
Core structures: space-less
We start by defining the Agent type and initializing the model.
using Agents
mutable struct WealthAgent <: AbstractAgent
id::Int
wealth::Int
end
Notice that this agent does not have a pos
field. That is okay, because there is no space structure to this example. We can also make a very simple AgentBasedModel
for our model.
function wealth_model(; numagents = 100, initwealth = 1)
model = ABM(WealthAgent, scheduler = random_activation)
for i in 1:numagents
add_agent!(model, initwealth)
end
return model
end
model = wealth_model()
AgentBasedModel with 100 agents of type WealthAgent no space scheduler: random_activation
The next step is to define the agent step function
function agent_step!(agent, model)
agent.wealth == 0 && return # do nothing
ragent = random_agent(model)
agent.wealth -= 1
ragent.wealth += 1
end
We use random_agent
as a convenient way to just grab a second agent. (this may return the same agent as agent
, but we don't care in the long run)
Running the space-less model
Let's do some data collection, running a large model for a lot of time
N = 5
M = 2000
adata = [:wealth]
model = wealth_model(numagents = M)
data, _ = run!(model, agent_step!, N; adata = adata)
data[(end - 20):end, :]
step | id | wealth | |
---|---|---|---|
Int64 | Int64 | Int64 | |
1 | 5 | 1980 | 1 |
2 | 5 | 1981 | 1 |
3 | 5 | 1982 | 2 |
4 | 5 | 1983 | 0 |
5 | 5 | 1984 | 2 |
6 | 5 | 1985 | 2 |
7 | 5 | 1986 | 0 |
8 | 5 | 1987 | 2 |
9 | 5 | 1988 | 2 |
10 | 5 | 1989 | 0 |
11 | 5 | 1990 | 1 |
12 | 5 | 1991 | 2 |
13 | 5 | 1992 | 0 |
14 | 5 | 1993 | 1 |
15 | 5 | 1994 | 0 |
16 | 5 | 1995 | 1 |
17 | 5 | 1996 | 0 |
18 | 5 | 1997 | 1 |
19 | 5 | 1998 | 0 |
20 | 5 | 1999 | 0 |
21 | 5 | 2000 | 2 |
What we mostly care about is the distribution of wealth, which we can obtain for example by doing the following query:
wealths = filter(x -> x.step == N - 1, data)[!, :wealth]
2000-element Array{Int64,1}: 0 0 1 1 0 1 0 0 1 3 ⋮ 1 2 0 2 1 0 0 0 1
and then we can make a histogram of the result. With a simple visualization we immediately see the power-law distribution:
using UnicodePlots
UnicodePlots.histogram(wealths)
┌ ┐ [ 0.0, 1.0) ┤▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 881 [ 1.0, 2.0) ┤▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 600 [ 2.0, 3.0) ┤▇▇▇▇▇▇▇▇▇▇▇▇ 300 [ 3.0, 4.0) ┤▇▇▇▇▇ 126 [ 4.0, 5.0) ┤▇▇ 59 [ 5.0, 6.0) ┤▇ 24 [ 6.0, 7.0) ┤ 7 [ 7.0, 8.0) ┤ 1 [ 8.0, 9.0) ┤ 1 [ 9.0, 10.0) ┤ 1 └ ┘ Frequency
Core structures: with space
We now expand this model to (in this case) a 2D grid. The rules are the same but agents exchange wealth only with their neighbors.
It is also available from the Models
module as Models.wealth_distribution
.
We therefore have to add a pos
field as the second field of the agents:
mutable struct WealthInSpace <: AbstractAgent
id::Int
pos::NTuple{2,Int}
wealth::Int
end
function wealth_model_2D(; dims = (25, 25), wealth = 1, M = 1000)
space = GridSpace(dims, periodic = true)
model = ABM(WealthInSpace, space; scheduler = random_activation)
for i in 1:M # add agents in random nodes
add_agent!(model, wealth)
end
return model
end
model2D = wealth_model_2D()
AgentBasedModel with 1000 agents of type WealthInSpace space: GridSpace with 625 nodes and 1250 edges scheduler: random_activation
The agent actions are a just a bit more complicated in this example. Now the agents can only give wealth to agents that exist on the same or neighboring nodes (their "neighbors").
function agent_step_2d!(agent, model)
agent.wealth == 0 && return # do nothing
agent_node = coord2vertex(agent.pos, model)
neighboring_nodes = node_neighbors(agent_node, model)
push!(neighboring_nodes, agent_node) # also consider current node
rnode = rand(neighboring_nodes) # the node that we will exchange with
available_ids = get_node_contents(rnode, model)
if length(available_ids) > 0
random_neighbor_agent = model[rand(available_ids)]
agent.wealth -= 1
random_neighbor_agent.wealth += 1
end
end
Running the model with space
init_wealth = 4
model = wealth_model_2D(; wealth = init_wealth)
adata = [:wealth, :pos]
data, _ = run!(model, agent_step!, 10; adata = adata, when = [1, 5, 9])
data[(end - 20):end, :]
step | id | wealth | pos | |
---|---|---|---|---|
Int64 | Int64 | Int64 | Tuple… | |
1 | 9 | 980 | 3 | (14, 7) |
2 | 9 | 981 | 1 | (14, 2) |
3 | 9 | 982 | 6 | (22, 6) |
4 | 9 | 983 | 7 | (20, 16) |
5 | 9 | 984 | 5 | (7, 22) |
6 | 9 | 985 | 3 | (15, 13) |
7 | 9 | 986 | 2 | (21, 1) |
8 | 9 | 987 | 3 | (2, 10) |
9 | 9 | 988 | 2 | (24, 1) |
10 | 9 | 989 | 8 | (24, 11) |
11 | 9 | 990 | 5 | (2, 13) |
12 | 9 | 991 | 2 | (5, 4) |
13 | 9 | 992 | 7 | (22, 18) |
14 | 9 | 993 | 0 | (14, 2) |
15 | 9 | 994 | 1 | (15, 6) |
16 | 9 | 995 | 1 | (25, 7) |
17 | 9 | 996 | 3 | (11, 5) |
18 | 9 | 997 | 5 | (15, 14) |
19 | 9 | 998 | 2 | (10, 6) |
20 | 9 | 999 | 3 | (5, 18) |
21 | 9 | 1000 | 0 | (18, 11) |
Okay, now we want to get the 2D spatial wealth distribution of the model. That is actually straightforward:
using Plots
function wealth_distr(data, model, n)
W = zeros(Int, size(model.space))
for row in eachrow(filter(r -> r.step == n, data)) # iterate over rows at a specific step
W[row.pos...] += row.wealth
end
return W
end
W1 = wealth_distr(data, model2D, 1)
Plots.heatmap(W1)
W5 = wealth_distr(data, model2D, 5)
Plots.heatmap(W5)
W10 = wealth_distr(data, model2D, 9)
Plots.heatmap(W10)
What we see is that wealth gets more and more localized.