From Max Weinreich, a purge puzzle:
A town of 1,000 households has a strange law intended to prevent wealth-hoarding. On January 1 of every year, each household robs one other household, selected at random, moving all of that house’s money into their own house. The order in which the robberies take place is also random and is determined by a lottery. (Note that if House A robs House B first, and then C robs A, the houses of A and B would each be empty and C would have acquired the resources of both A and B.)
Two questions about this fateful day:
In [1]:
from itertools import repeat, cycle, chain, count
from collections import defaultdict
import matplotlib.pyplot as plt
import matplotlib.cm as cm
import matplotlib as mpl
from tqdm import tqdm_notebook
from multiprocessing import Pool
import matplotlib.animation as animation
import numpy as np
In [2]:
def calculate_all_expecteds(houses):
return list(map(calculate_expected, houses))
In [3]:
def calculate_expected(house):
return sum([amt*odd for amt,odd in house.items()])
In [4]:
def possible_amounts(all_houses, ignore_index, num_houses):
possibles = defaultdict(float)
for j, house in enumerate(all_houses):
if j != ignore_index:
for amount, odd in house.items():
possibles[amount] += odd
return {a: o / (num_houses - 1) for a, o in possibles.items()}
In [5]:
def adjust_thief_odds(house, possibles):
resulting_probs = []
for h_amount, h_odd in house.items():
for p_amount, p_odd in possibles.items():
resulting_probs.append((h_amount + p_amount, h_odd * p_odd))
new_house = defaultdict(float)
for r_amount, r_odd in resulting_probs:
new_house[r_amount] += r_odd
return dict(new_house)
In [6]:
def adjust_victim_house(house, num_houses):
total = 0
new_house = defaultdict(float)
for amount, odd in house.items():
temp_odd = odd * (1 - (1/(num_houses - 1)))
new_house[amount] = temp_odd
total += temp_odd
new_house[0] += 1 - total
return dict(new_house)
In [7]:
def run_simulation(num_houses, initial_amount):
all_houses = list(repeat({initial_amount: 1.0}, num_houses))
for i, thief_house in enumerate(tqdm_notebook(all_houses, desc='calculation')):
# calculate possible amounts that could be stolen, probability of each
possibles = possible_amounts(all_houses, i, num_houses)
# adjust probabilities of each amount in selected house
all_houses[i] = adjust_thief_odds(thief_house, possibles)
# add probability to each other house that their amount was stolen
for j, victim_house in enumerate(all_houses):
if j != i:
all_houses[j] = adjust_victim_house(victim_house, num_houses)
yield calculate_all_expecteds(all_houses)
In [71]:
def generate_evolution_figure(expecteds):
num_houses = len(expecteds)
fig, ax = plt.subplots(figsize=(12, 8))
ax.set_xlim([0, num_houses])
ax.set_ylim([0, max(expecteds[1])])
ax.set_xlabel("House Index")
ax.set_ylabel("Expected Amount [$]")
ax.set_title("Expected Amount in House over time")
bar = ax.bar(range(num_houses), expecteds[0], width=1, align='center')
fit_line, = ax.plot([],[], 'r--', linewidth=3)
return fig, bar, fit_line
def update_evolution_figure(frame_num,
fig,
bar,
fit_line,
norm,
expected_len,
expected_generator,
pbar):
exp = next(expected_generator)
for i, b in enumerate(bar):
b.set_height(exp[i])
b.set_facecolor(color=cm.ScalarMappable(norm=norm, cmap=cm.viridis).to_rgba(exp[i]))
if frame_num >= expected_len:
x = range(len(exp))
p = np.poly1d(np.polyfit(x, exp, 2))
fit_line.set_data(x, p(x))
pbar.update(1)
def plot_expected_evolution(expecteds, output_file, fps, repeat_delay):
fig, bar, fit_line = generate_evolution_figure(expecteds)
norm = mpl.colors.Normalize(vmin=0, vmax=max(expecteds[1]))
with tqdm_notebook(total=len(expecteds)+repeat_delay) as pbar:
evo_ani = animation.FuncAnimation(fig,
update_evolution_figure,
fargs=(fig,
bar,
fit_line,
norm,
len(expecteds),
cycle(chain(expecteds,
repeat(expecteds[-1],
repeat_delay))),
pbar),
repeat=True,
save_count=len(expecteds)+repeat_delay)
evo_ani.save(output_file, writer="imagemagick", fps=fps)
In [72]:
NUM_HOUSES = 1000
INIT_AMOUNT = 100
In [73]:
expecteds = list(run_simulation(NUM_HOUSES, INIT_AMOUNT))
In [75]:
plot_expected_evolution(expecteds,'evolution.gif', 48, 100)
plt.close('all')