Economics Simulation

This is a simulation of an economic marketplace in which there is a population of actors, each of which has a level of wealth (a single number) that changes over time. On each time step two agents (chosen by an interaction rule) interact with each other and exchange wealth (according to a transaction rule). The idea is to understand the evolution of the population's wealth over time. My hazy memory is that this idea came from a class by Prof. Sven Anderson at Bard (any errors or misconceptions here are due to my (Peter Norvig) misunderstanding of his idea). Why this is interesting: (1) an example of using simulation to model the world. (2) Many students will have preconceptions about how economies work that will be challenged by the results shown here.

Population Distributions

First things first: what should our initial population look like? We will provide several distribution functions (constant, uniform, Gaussian, etc.) and a sample function, which samples N elements form a distribution and then normalizes them to have a given mean. By default we will have N=5000 actors and an initial mean wealth of 100 simoleons.

In [299]:
import random
import matplotlib
import matplotlib.pyplot as plt

N  = 5000 # Default size of population
mu = 100. # Default mean of population's wealth

def sample(distribution, N=N, mu=mu):
    "Sample from the distribution N times, then normalize results to have mean mu."
    return normalize([distribution() for _ in range(N)], mu * N)

def constant(mu=mu):          return mu
def uniform(mu=mu, width=mu): return random.uniform(mu-width/2, mu+width/2)
def gauss(mu=mu, sigma=mu/3): return random.gauss(mu, sigma) 
def beta(alpha=2, beta=3):    return random.betavariate(alpha, beta)
def pareto(alpha=4):          return random.paretovariate(alpha)
    
def normalize(numbers, total):
    "Scale the numbers so that they add up to total."
    factor = total / float(sum(numbers))
    return [x * factor for x in numbers]

Transactions

In a transaction, two actors come together; they have existing wealth levels X and Y. For now we will only consider transactions that conserve wealth, so our transaction rules will decide how to split up the pot of X+Y total wealth.

In [360]:
def random_split(X, Y):
    "Take all the money in the pot and divide it randomly between X and Y."
    pot = X + Y
    m = random.uniform(0, pot)
    return m, pot - m
    
def winner_take_most(X, Y, most=3/4.): 
    "Give most of the money in the pot to one of the parties."
    pot = X + Y
    m = random.choice((most * pot, (1 - most) * pot))
    return m, pot - m

def winner_take_all(X, Y): 
    "Give all the money in the pot to one of the actors."
    return winner_take_most(X, Y, 1.0)

def redistribute(X, Y): 
    "Give 55% of the pot to the winner; 45% to the loser."
    return winner_take_most(X, Y, 0.55)

def split_half_min(X, Y):
    """The poorer actor only wants to risk half his wealth; 
    the other actor matches this; then we randomly split the pot."""
    pot = min(X, Y)
    m = random.uniform(0, pot)
    return X - pot/2. + m, Y + pot/2. - m

Interactions

How do you decide which parties interact with each other? The rule anyone samples two members of the population uniformly and independently, but there are other possible rules, like nearby(pop, k), which choses one member uniformly and then chooses a second within k index elements away, to simulate interactions within a local neighborhood.

In [356]:
def anyone(pop): return random.sample(range(len(pop)), 2)

def nearby(pop, k=5): 
    i = random.randrange(len(pop))
    j = i + random.choice((1, -1)) * random.randint(1, k)
    return i, (j % len(pop))
               
def nearby1(pop): return nearby(pop, 1)

Simulation

Now let's describe the code to run the simulation and summarize/plot the results. The function simulate does the work; it runs the interaction function to find two actors, then calls the transaction function to figure out how to split their wealth, and repeats this T times. The only other thing it does is record results. Every so-many steps, it records some summary statistics of the population (by default, this will be every 25 steps).

What information do we record to summarize the population? Out of the N=5000 (by default) actors, we will record the wealth of exactly nine of them: the ones, in sorted-by-wealth order that occupy the 1% spot (that is, if N=5000, this would be the 50th wealthiest actor), then the 10%, 25% 1/3, and median; and then likewise from the bottom the 1%, 10%, 25% and 1/3.

(Note that we record the median, which changes over time; the mean is defined to be 100 when we start, and since all transactions conserve wealth, the mean will always be 100.)

What do we do with these results, once we have recorded them? First we print them in a table for the first time step, the last, and the middle. Then we plot them as nine lines in a plot where the y-axis is wealth and the x-axis is time (note that when the x-axis goes from 0 to 1000, and we have record_every=25, that means we have actually done 25,000 transactions, not 1000).

In [368]:
def simulate(population, transaction_fn, interaction_fn, T, percentiles, record_every):
    "Run simulation for T steps; collect percentiles every 'record_every' time steps."
    results = []
    for t in range(T):
        i, j = interaction_fn(population)
        population[i], population[j] = transaction_fn(population[i], population[j]) 
        if t % record_every == 0:
            results.append(record_percentiles(population, percentiles))
    return results

def report(distribution=gauss, transaction_fn=random_split, interaction_fn=anyone, N=N, mu=mu, T=5*N, 
           percentiles=(1, 10, 25, 33.3, 50, -33.3, -25, -10, -1), record_every=25):
    "Print and plot the results of the simulation running T steps." 
    # Run simulation
    population = sample(distribution, N, mu)
    results = simulate(population, transaction_fn, interaction_fn, T, percentiles, record_every)
    # Print summary
    print('Simulation: {} * {}(mu={}) for T={} steps with {} doing {}:\n'.format(
          N, name(distribution), mu, T, name(interaction_fn), name(transaction_fn)))
    fmt = '{:6}' + '{:10.2f} ' * len(percentiles)
    print(('{:6}' + '{:>10} ' * len(percentiles)).format('', *map(percentile_name, percentiles)))
    for (label, nums) in [('start', results[0]), ('mid', results[len(results)//2]), ('final', results[-1])]:
        print fmt.format(label, *nums)
    # Plot results
    for line in zip(*results):
        plt.plot(line)
    plt.show()

def record_percentiles(population, percentiles):
    "Pick out the percentiles from population."
    population = sorted(population, reverse=True)
    N = len(population)
    return [population[int(p*N/100.)] for p in percentiles]

def percentile_name(p):
    return ('median' if p == 50 else 
            '{} {}%'.format(('top' if p > 0 else 'bot'), abs(p)))
    
def name(obj):
    return getattr(obj, '__name__', str(obj))

Simulations Results

Finally, let's run a simulation!

In [369]:
report(gauss, random_split)
Simulation: 5000 * gauss(mu=100.0) for T=25000 steps with anyone doing random_split:

          top 1%    top 10%    top 25%  top 33.3%     median  bot 33.3%    bot 25%    bot 10%     bot 1% 
start     177.13     141.87     123.07     114.48     100.18      85.87      77.55      56.98      20.87 
mid       411.63     221.87     139.95     114.71      74.20      45.50      32.80      12.13       1.16 
final     447.98     228.06     140.54     111.17      69.66      40.74      28.77      10.84       1.29 

How do we interpret this? Well, we can see the mass of wealth spreading out: the rish get richer and the poor get poorer. We know the rich get richer because the blue and green lines (top 10% and top 1%) are going up: the actor in the 1% position (the guy with the least money out of the 1%, or to put it another way, the most money out of the 99%) starts with 177.13 and ends up with 447.98 (note this is not necessarily the same guy, just the guy who ends up in that position). The guy at the 10% spot also gets richer, going from 141.87 to 228.06. The 25% and 33% marks stay roughly flat, but everyone else gets poorer! The median actor loses 30% of his wealth, and the bottom 1% actor loses almost 95% of his wealth.

Effect of Starting Population

Now let's see if the starting population makes any difference. My vague impression is that we're dealing with ergodic Markov chains and it doesn't much matter what state you start in. But let's see:

In [376]:
report(uniform, random_split)
report(beta, random_split)
report(pareto, random_split)
report(constant, random_split)
Simulation: 5000 * uniform(mu=100.0) for T=25000 steps with anyone doing random_split:

          top 1%    top 10%    top 25%  top 33.3%     median  bot 33.3%    bot 25%    bot 10%     bot 1% 
start     149.34     140.15     125.30     117.02      99.77      83.05      74.46      59.76      51.09 
mid       411.50     224.90     140.78     114.67      74.63      45.26      32.23      12.32       1.19 
final     452.55     224.37     140.10     111.70      72.22      41.30      29.02      10.15       0.97 
Simulation: 5000 * beta(mu=100.0) for T=25000 steps with anyone doing random_split:

          top 1%    top 10%    top 25%  top 33.3%     median  bot 33.3%    bot 25%    bot 10%     bot 1% 
start     216.56     169.28     135.63     121.32      96.21      74.52      61.30      35.51      11.25 
mid       428.48     221.72     141.16     113.06      74.22      44.32      31.43      11.15       1.32 
final     444.30     229.18     139.71     111.27      70.48      41.29      29.25      11.19       0.95 
Simulation: 5000 * pareto(mu=100.0) for T=25000 steps with anyone doing random_split:

          top 1%    top 10%    top 25%  top 33.3%     median  bot 33.3%    bot 25%    bot 10%     bot 1% 
start     233.06     134.23     106.16      98.62      88.98      82.95      80.69      76.99      75.07 
mid       405.27     219.39     140.12     114.21      76.42      46.14      33.19      12.38       1.05 
final     456.02     229.86     138.44     110.59      70.45      41.43      28.62      10.39       1.08 
Simulation: 5000 * constant(mu=100.0) for T=25000 steps with anyone doing random_split:

          top 1%    top 10%    top 25%  top 33.3%     median  bot 33.3%    bot 25%    bot 10%     bot 1% 
start     100.00     100.00     100.00     100.00     100.00     100.00     100.00     100.00     100.00 
mid       399.48     218.92     143.25     116.62      75.67      45.47      31.75      12.09       1.58 
final     432.62     232.54     140.34     110.91      70.88      41.38      30.11      11.01       0.97 

It looks like we can confirm that the starting population doesn't matter much—if we are using the random_split rule then in the end, wealth accumulates to the top third at the expense of the bottom two-thirds, regardless of starting population.

Effect of Transaction Rule

Now let's see what happens when we vary the transaction rule. The random_split rule produces inequality: the actor at the bottom quarter of the population has only about a third of the mean wealth, and the actor at the top 1% spot has 4.5 times the mean. Suppose we want a society with more income equality. We could use the split_half_min rule, in which each transaction has a throttle in that the poorer party only risks half of their remaining wealth. Or we could use the redistribute rule, in which the loser of a transaction still gets 45% of the total (meaning the loser will actually gain in many transactions). Let's see what effects these rules have. In analyzing these plots, note that they have different Y-axes.

In [371]:
report(gauss, random_split)
report(gauss, redistribute)
report(gauss, split_half_min)
Simulation: 5000 * gauss(mu=100.0) for T=25000 steps with anyone doing random_split:

          top 1%    top 10%    top 25%  top 33.3%     median  bot 33.3%    bot 25%    bot 10%     bot 1% 
start     176.83     142.52     123.30     114.87     100.34      84.89      77.03      57.48      20.69 
mid       397.50     224.36     141.36     114.86      73.78      45.44      32.89      11.94       1.41 
final     443.51     228.02     139.65     111.89      71.17      42.30      29.84      10.92       1.53 
Simulation: 5000 * gauss(mu=100.0) for T=25000 steps with anyone doing redistribute:

          top 1%    top 10%    top 25%  top 33.3%     median  bot 33.3%    bot 25%    bot 10%     bot 1% 
start     178.43     142.71     122.02     114.10      99.72      85.78      77.43      57.60      23.46 
mid       141.66     121.81     110.79     106.43      98.89      91.64      88.19      79.96      65.28 
final     134.38     119.82     110.18     105.78      98.81      92.49      89.14      82.07      72.51 
Simulation: 5000 * gauss(mu=100.0) for T=25000 steps with anyone doing split_half_min:

          top 1%    top 10%    top 25%  top 33.3%     median  bot 33.3%    bot 25%    bot 10%     bot 1% 
start     179.22     142.88     122.64     114.48     100.49      85.23      76.89      57.61      20.20 
mid       277.50     184.92     137.30     120.01      89.51      64.45      52.42      29.22       8.80 
final     325.56     211.06     144.15     119.62      80.44      51.06      38.69      17.50       3.98 

We see that the redistribute rule is very effective in reducing income inequality: the lines of the plot all converge towards the mean of 100 instead of diverging. With the split_half_min rule, inequality increases at a rate about half as fast as random_split. However, the split_half_min plot looks like it hasn't converged yet (whereas all the other plots reach convergence at about the 500 mark). Let's try running split_half_min 10 times longer:

In [372]:
report(gauss, split_half_min, T=50*N)
Simulation: 5000 * gauss(mu=100.0) for T=250000 steps with anyone doing split_half_min:

          top 1%    top 10%    top 25%  top 33.3%     median  bot 33.3%    bot 25%    bot 10%     bot 1% 
start     179.49     142.66     122.22     113.85      99.91      85.38      77.50      57.61      22.80 
mid       695.22     305.73     128.25      80.00      31.85      11.42       5.69       1.11       0.05 
final     980.29     330.54      90.61      43.87      10.60       2.15       0.77       0.07       0.00 

It looks like split_half_min still hasn't converged, and is continuing to (slowly) drive wealth to the top 10%.

Now let's shift gears: suppose that we don't care about decreasing income inequality; instead we want to increase opportunity for some actors to become wealthier. We can try the winner_take_most or winner_take_all rules (compared to the baseline random_split):

In [373]:
report(gauss, random_split)
report(gauss, winner_take_most)
report(gauss, winner_take_all)
Simulation: 5000 * gauss(mu=100.0) for T=25000 steps with anyone doing random_split:

          top 1%    top 10%    top 25%  top 33.3%     median  bot 33.3%    bot 25%    bot 10%     bot 1% 
start     179.15     142.41     122.16     115.14      99.60      85.68      77.36      57.15      22.66 
mid       391.59     220.50     142.05     117.68      76.85      45.21      33.11      12.83       1.10 
final     458.68     231.19     139.85     109.79      69.74      41.12      28.40      10.73       1.15 
Simulation: 5000 * gauss(mu=100.0) for T=25000 steps with anyone doing winner_take_most:

          top 1%    top 10%    top 25%  top 33.3%     median  bot 33.3%    bot 25%    bot 10%     bot 1% 
start     178.63     142.45     123.04     114.70      99.82      85.52      77.20      56.57      22.87 
mid       350.29     210.58     135.68     110.43      75.48      53.93      43.55      26.96      14.01 
final     368.07     216.06     131.68     106.61      74.65      51.83      41.93      25.97      12.12 
Simulation: 5000 * gauss(mu=100.0) for T=25000 steps with anyone doing winner_take_all:

          top 1%    top 10%    top 25%  top 33.3%     median  bot 33.3%    bot 25%    bot 10%     bot 1% 
start     176.20     144.07     122.86     114.32      99.70      85.47      77.00      56.55      21.64 
mid      1058.80     358.07      91.22       0.00       0.00       0.00       0.00       0.00       0.00 
final    1655.55     307.09       0.00       0.00       0.00       0.00       0.00       0.00       0.00 

We see that the winner_take_most rule, in which the winner of a transaction takes 3/4 of the pot, does not increase the opportunity for wealth as much as random_split, but that winner_take_all is very effective at concentrating almost all the wealth in the hands of the top 10%, and makes the top 1% 4 times as wealthy as random_split.

That suggests we look at where the breaking point is. Let's consider several different amounts for what winner takes:

In [375]:
def winner_take_80(X, Y): return winner_take_most(X, Y, 0.80)
def winner_take_90(X, Y): return winner_take_most(X, Y, 0.90)
def winner_take_95(X, Y): return winner_take_most(X, Y, 0.95)

report(gauss, winner_take_80)
report(gauss, winner_take_90)
report(gauss, winner_take_95)
Simulation: 5000 * gauss(mu=100.0) for T=25000 steps with anyone doing winner_take_80:

          top 1%    top 10%    top 25%  top 33.3%     median  bot 33.3%    bot 25%    bot 10%     bot 1% 
start     181.52     142.83     121.99     114.49      99.42      84.84      76.82      56.83      23.19 
mid       425.14     240.72     135.19     101.46      64.98      42.87      33.22      17.22       6.99 
final     478.41     251.16     126.46      97.69      61.18      38.04      28.47      15.49       6.29 
Simulation: 5000 * gauss(mu=100.0) for T=25000 steps with anyone doing winner_take_90:

          top 1%    top 10%    top 25%  top 33.3%     median  bot 33.3%    bot 25%    bot 10%     bot 1% 
start     177.39     142.90     123.46     115.39     100.39      85.71      77.49      55.82      25.07 
mid       680.75     312.72     114.75      69.06      36.13      17.88      11.36       4.41       1.04 
final     839.35     311.05      95.08      65.70      31.85      13.24       8.69       3.28       0.66 
Simulation: 5000 * gauss(mu=100.0) for T=25000 steps with anyone doing winner_take_95:

          top 1%    top 10%    top 25%  top 33.3%     median  bot 33.3%    bot 25%    bot 10%     bot 1% 
start     177.80     142.69     121.76     113.88     100.17      85.20      77.64      58.08      23.10 
mid       806.09     339.73     112.24      43.93      19.07       8.56       4.66       1.07       0.14 
final    1061.58     323.33      59.14      37.67      15.58       4.43       2.45       0.78       0.09 

We see that winner takes 80% produces results similar to random_split, and that winner takes 95% is similar to winner takes all for the top 10%, but is much kinder to the bottom 75%.

Effect of Interactions

Suppose that transactions are constrained to be local; that you can only do business with your close neighbors. Will that make income more equitable, because there will be no large, global conglomorates? Let's see:

In [374]:
report(gauss, random_split, anyone)
report(gauss, random_split, nearby)
report(gauss, random_split, nearby1)
Simulation: 5000 * gauss(mu=100.0) for T=25000 steps with anyone doing random_split:

          top 1%    top 10%    top 25%  top 33.3%     median  bot 33.3%    bot 25%    bot 10%     bot 1% 
start     177.73     142.78     122.06     114.12     100.05      85.83      78.13      56.99      23.92 
mid       406.19     218.91     141.11     115.21      74.91      44.09      32.03      12.66       1.05 
final     448.86     229.76     137.90     110.64      71.69      41.12      28.10      10.15       0.97 
Simulation: 5000 * gauss(mu=100.0) for T=25000 steps with nearby doing random_split:

          top 1%    top 10%    top 25%  top 33.3%     median  bot 33.3%    bot 25%    bot 10%     bot 1% 
start     175.29     142.11     123.32     115.58     100.02      85.23      77.28      56.46      22.99 
mid       392.89     220.22     142.93     115.24      75.46      44.72      33.64      12.54       1.41 
final     417.99     227.64     140.53     112.70      72.89      43.47      30.52      11.79       1.09 
Simulation: 5000 * gauss(mu=100.0) for T=25000 steps with nearby1 doing random_split:

          top 1%    top 10%    top 25%  top 33.3%     median  bot 33.3%    bot 25%    bot 10%     bot 1% 
start     177.55     142.07     122.37     113.99     100.02      85.68      77.74      57.62      23.26 
mid       353.05     211.08     142.84     119.68      81.54      50.33      36.31      14.11       1.63 
final     377.13     221.84     145.43     117.31      76.45      45.68      32.32      12.30       0.93 

We see that the nearby rule, which limits transactions to your 5 closest neighbors in either direction (out of 5000 total actors), has a negligible effect on the outcome. I found that fairly surprising. But the nearby1 rule, which lets you do business only with your immediate left or right neighbor does have a slight effect towards income equality. The bottom quarter still do poorly, but the top 1% only gets to about 85% of what they get under unconstrained trade.

In []: