Setup

In [1]:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import random
from collections import namedtuple

%matplotlib inline
In [2]:
NUM_GAMES = 10000
NUM_ROUNDS = 100

results = []
In [3]:
NUM_PROPERTIES = 40
GO = 0
GO_TO_JAIL = 30
JAIL = 10
BOARDWALK = 39
READING = 5
ILLINOIS = 25
ST_CHARLES = 11
CHANCE = (7, 22, 36)
COMMUNITY_CHEST = (2, 17, 33)
UTILITIES = (12, 28)
RAILROADS = (5, 15, 25, 35)
In [4]:
Property = namedtuple('Property', ['name', 'color', 'low_rent', 'high_rent', 'cost', 'full_cost'])
In [5]:
properties = [
    Property('Go', 'None', 0, 0, 1, 1),  # Non-purchasable have cost of 1 to prevent div-zero errors later.
    Property('Mediterranean Avenue', 'Dark Purple', 2, 250, 60, 310),
    Property('Community Chest', 'None', 0, 0, 1, 1),
    Property('Baltic Avenue', 'Dark Purple', 4, 320, 60, 310),
    Property('Income Tax', 'None', 0, 0, 1, 1),
    Property('Reading Railroad', 'Railroad', 25, 200, 200, 200),
    Property('Oriental Avenue', 'Light Blue', 6, 550, 100, 350),
    Property('Chance', 'None', 0, 0, 1, 1),
    Property('Vermont Avenue', 'Light Blue', 6, 550, 100, 350),
    Property('Connecticut Avenue', 'Light Blue', 8, 600, 120, 370),
    Property('Jail', 'None', 0, 0, 1, 1),
    Property('St. Charles Place', 'Pink', 10, 750, 140, 640),
    Property('Electric Company', 'Utility', 28, 70, 150, 150),  # Rent based on mean roll of 7.
    Property('States Avenue', 'Pink', 10, 750, 140, 640),
    Property('Virginia Avenue', 'Pink', 12, 900, 160, 660),
    Property('Pennsylvania Railroad', 'Railroad', 25, 200, 200, 200),
    Property('St. James Place', 'Orange', 14, 950, 180, 680),
    Property('Community Chest', 'None', 0, 0, 1, 1),
    Property('Tennessee Avenue', 'Orange', 14, 950, 180, 680),
    Property('New York Avenue', 'Orange', 16, 1000, 200, 700),
    Property('Free Parking', 'None', 0, 0, 1, 1),
    Property('Kentucky Avenue', 'Red', 18, 1050, 220, 970),
    Property('Chance', 'None', 0, 0, 1, 1),
    Property('Indiana Avenue', 'Red', 18, 1050, 220, 970),
    Property('Illinois Avenue', 'Red', 20, 1100, 240, 990),
    Property('B&O Railroad', 'Railroad', 25, 200, 200, 200),
    Property('Atlantic Avenue', 'Yellow', 22, 1150, 260, 1010),
    Property('Ventnor Avenue', 'Yellow', 22, 1150, 260, 1010),
    Property('Water Works', 'Utility', 28, 70, 150, 150),  # Rent based on mean roll of 7.
    Property('Marvin Garden', 'Yellow', 24, 1200, 280, 1030),
    Property('Go To Jail', 'None', 0, 0, 1, 1),
    Property('Pacific Avenue', 'Green', 26, 1275, 300, 1300),
    Property('North Carolina Avenue', 'Green', 26, 1275, 300, 1300),
    Property('Community Chest', 'None', 0, 0, 1, 1),
    Property('Pennsylvania Avenue', 'Green', 28, 1400, 320, 1320),
    Property('Short Line', 'Railroad', 25, 200, 200, 200), 
    Property('Chance', 'None', 0, 0, 1, 1),
    Property('Park Place', 'Blue', 35, 1500, 350, 1350),
    Property('Luxury Tax', 'None', 0, 0, 1, 1),
    Property('Boardwalk', 'Blue', 50, 2000, 400, 1400)
]
In [6]:
class Player():
    
    def __init__(self, number, game):
        self.number = number
        self.game = game
        self.position = 0
        self.turn = 0
        self.round = 1
    
    def roll_dice(self):
        dice_one = random.randint(1, 6)
        dice_two = random.randint(1, 6)

        return dice_one + dice_two, dice_one == dice_two
    
    def go_to_property(self, number, pass_go=True):
        if pass_go and self.position > number:
            self.round += 1
        
        self.position = number
        
        results.append([self.game.number, 
                        self.number, 
                        self.turn, 
                        0, 
                        False, 
                        self.position, 
                        properties[self.position].name])
        
        self.resolve_property()

    def take_turn(self):
        global df
        
        self.turn += 1
        doubles_count = 0
        
        while True:
            roll, doubles = self.roll_dice()
            
            if doubles:
                doubles_count += 1
            
            if doubles_count >= 3:
                self.go_to_property(JAIL, pass_go=False)
                break
            
            self.position += roll
            
            if self.position >= NUM_PROPERTIES:
                self.position = self.position % NUM_PROPERTIES
                self.round += 1
            
            results.append([self.game.number, 
                            self.number, 
                            self.turn, 
                            roll, 
                            doubles, 
                            self.position, 
                            properties[self.position].name])
            
            end_turn = self.resolve_property()
            
            if end_turn:
                break
                    
            if not doubles:
                break
    
    def resolve_property(self):
        
        if self.position == GO_TO_JAIL:
            self.go_to_property(JAIL, pass_go=False)
            return True
            
        elif self.position in CHANCE:
            card = self.game.draw_chance()

            if card == 0:  # Advance to Go
                self.go_to_property(GO)
            elif card == 1:  # Go to Jail
                self.go_to_property(JAIL, pass_go=False)
                return True
            elif card == 2:  # Go Back 3 Spaces
                new_position = self.position - 3
                if new_position < 0:
                    new_position += NUM_PROPERTIES
                self.go_to_property(new_position)
            elif card == 3:  # Advance to Boardwalk
                self.go_to_property(BOARDWALK)
            elif card == 4:  # Advance to Reading Railroad
                self.go_to_property(READING)
            elif card == 5:  # Advance to Illinois Avenue
                self.go_to_property(ILLINOIS)
            elif card == 6:  # Advance to St. Charles Place
                self.go_to_property(ST_CHARLES)
            elif card == 7:  # Advance to nearest Utility
                found = False
                for util in UTILITIES:
                    if self.position < util:
                        found = True
                        self.go_to_property(util)
                
                if not found:
                    self.go_to_property(UTILITIES[0])
            elif card == 8 or card == 9:
                found = False
                for rr in RAILROADS:
                    if self.position < rr:
                        found = True
                        self.go_to_property(rr)
                
                if not found:
                    self.go_to_property(RAILROADS[0])
                    

        elif self.position in COMMUNITY_CHEST:
            card = self.game.draw_community_chest()

            if card == 0:  # Advance to Go
                self.go_to_property(GO)
            elif card == 1:  # Go to Jail
                self.go_to_property(JAIL, pass_go=False)
                return True
        
        return False
In [7]:
class Game():
    
    def __init__(self, number, num_players):
        self.number = number
        self.players = [Player(i + 1, self) for i in range(num_players)]
        self.chance = [i for i in range(16)]
        random.shuffle(self.chance)
        self.community_chest = [i for i in range(16)]
        random.shuffle(self.community_chest)
    
    def play_game(self, num_turns):
        for i in range(num_turns):
            for p in self.players:
                p.take_turn()
    
    def draw_card(self, deck):
        card = deck.pop(0)
        deck.append(card)
        return card
    
    def draw_community_chest(self):
        return self.draw_card(self.community_chest)

    def draw_chance(self):
        return self.draw_card(self.chance)

Determine Return on Investment Rate

In [8]:
investments = pd.DataFrame(properties, columns=['Name', 'Color', 'Low Rent', 'High Rent', 'Low Cost', 'High Cost'])
In [9]:
investments.head()
Out[9]:
Name Color Low Rent High Rent Low Cost High Cost
0 Go None 0 0 1 1
1 Mediterranean Avenue Dark Purple 2 250 60 310
2 Community Chest None 0 0 1 1
3 Baltic Avenue Dark Purple 4 320 60 310
4 Income Tax None 0 0 1 1
In [10]:
investments['Low ROI'] = investments['Low Cost'] / investments['Low Rent']
investments['High ROI'] = investments['High Cost'] / investments['High Rent']
In [11]:
columns = ['Name', 'Low Cost', 'Low Rent', 'Low ROI']
investments.sort('Low ROI')[columns]
Out[11]:
Name Low Cost Low Rent Low ROI
28 Water Works 150 28 5.357143
12 Electric Company 150 28 5.357143
25 B&O Railroad 200 25 8.000000
15 Pennsylvania Railroad 200 25 8.000000
5 Reading Railroad 200 25 8.000000
39 Boardwalk 400 50 8.000000
35 Short Line 200 25 8.000000
37 Park Place 350 35 10.000000
34 Pennsylvania Avenue 320 28 11.428571
31 Pacific Avenue 300 26 11.538462
32 North Carolina Avenue 300 26 11.538462
29 Marvin Garden 280 24 11.666667
27 Ventnor Avenue 260 22 11.818182
26 Atlantic Avenue 260 22 11.818182
24 Illinois Avenue 240 20 12.000000
23 Indiana Avenue 220 18 12.222222
21 Kentucky Avenue 220 18 12.222222
19 New York Avenue 200 16 12.500000
18 Tennessee Avenue 180 14 12.857143
16 St. James Place 180 14 12.857143
14 Virginia Avenue 160 12 13.333333
13 States Avenue 140 10 14.000000
11 St. Charles Place 140 10 14.000000
3 Baltic Avenue 60 4 15.000000
9 Connecticut Avenue 120 8 15.000000
6 Oriental Avenue 100 6 16.666667
8 Vermont Avenue 100 6 16.666667
1 Mediterranean Avenue 60 2 30.000000
22 Chance 1 0 inf
17 Community Chest 1 0 inf
7 Chance 1 0 inf
30 Go To Jail 1 0 inf
20 Free Parking 1 0 inf
4 Income Tax 1 0 inf
33 Community Chest 1 0 inf
38 Luxury Tax 1 0 inf
2 Community Chest 1 0 inf
36 Chance 1 0 inf
10 Jail 1 0 inf
0 Go 1 0 inf
In [12]:
columns = ['Name', 'High Cost', 'High Rent', 'High ROI']
investments.sort('High ROI')[columns]
Out[12]:
Name High Cost High Rent High ROI
9 Connecticut Avenue 370 600 0.616667
8 Vermont Avenue 350 550 0.636364
6 Oriental Avenue 350 550 0.636364
19 New York Avenue 700 1000 0.700000
39 Boardwalk 1400 2000 0.700000
18 Tennessee Avenue 680 950 0.715789
16 St. James Place 680 950 0.715789
14 Virginia Avenue 660 900 0.733333
11 St. Charles Place 640 750 0.853333
13 States Avenue 640 750 0.853333
29 Marvin Garden 1030 1200 0.858333
27 Ventnor Avenue 1010 1150 0.878261
26 Atlantic Avenue 1010 1150 0.878261
37 Park Place 1350 1500 0.900000
24 Illinois Avenue 990 1100 0.900000
23 Indiana Avenue 970 1050 0.923810
21 Kentucky Avenue 970 1050 0.923810
34 Pennsylvania Avenue 1320 1400 0.942857
3 Baltic Avenue 310 320 0.968750
15 Pennsylvania Railroad 200 200 1.000000
35 Short Line 200 200 1.000000
25 B&O Railroad 200 200 1.000000
5 Reading Railroad 200 200 1.000000
32 North Carolina Avenue 1300 1275 1.019608
31 Pacific Avenue 1300 1275 1.019608
1 Mediterranean Avenue 310 250 1.240000
12 Electric Company 150 70 2.142857
28 Water Works 150 70 2.142857
33 Community Chest 1 0 inf
36 Chance 1 0 inf
22 Chance 1 0 inf
4 Income Tax 1 0 inf
20 Free Parking 1 0 inf
38 Luxury Tax 1 0 inf
17 Community Chest 1 0 inf
10 Jail 1 0 inf
2 Community Chest 1 0 inf
7 Chance 1 0 inf
30 Go To Jail 1 0 inf
0 Go 1 0 inf

Run the Monte Carlo Simulation

In [13]:
for i in range(NUM_GAMES):
    g = Game(i, 4)
    g.play_game(NUM_ROUNDS)
In [14]:
game_results = pd.DataFrame(results, columns=['Game', 'Player', 'Turn', 'Roll', 'Doubles', 'Position', 'Property'])
In [15]:
game_results.head()
Out[15]:
Game Player Turn Roll Doubles Position Property
0 0 1 1 7 False 7 Chance
1 0 1 1 0 False 4 Income Tax
2 0 2 1 9 False 9 Connecticut Avenue
3 0 3 1 5 False 5 Reading Railroad
4 0 4 1 5 False 5 Reading Railroad

Count total hits per property

In [16]:
prop_revenue = pd.DataFrame({'Count' : game_results.groupby( ['Position', 'Property'] ).size()}).reset_index()
prop_revenue['Color'] = prop_revenue['Position'].apply(lambda x: properties[x].color)
prop_revenue['Percent Total Count'] = prop_revenue['Count'].apply(lambda x: x / prop_revenue['Count'].sum())
prop_revenue.head()
Out[16]:
Position Property Count Color Percent Total Count
0 0 Go 146917 None 0.028314
1 1 Mediterranean Avenue 101971 Dark Purple 0.019652
2 2 Community Chest 104252 None 0.020091
3 3 Baltic Avenue 105808 Dark Purple 0.020391
4 4 Income Tax 114672 None 0.022099
In [17]:
prop_revenue[['Property', 'Count', 'Percent Total Count']].sort('Count', ascending=False)
Out[17]:
Property Count Percent Total Count
10 Jail 295199 0.056890
25 B&O Railroad 179864 0.034663
0 Go 146917 0.028314
19 New York Avenue 146168 0.028169
5 Reading Railroad 144861 0.027917
35 Short Line 142711 0.027503
15 Pennsylvania Railroad 141960 0.027358
17 Community Chest 141822 0.027332
18 Tennessee Avenue 139522 0.026889
28 Water Works 136729 0.026350
20 Free Parking 135705 0.026153
16 St. James Place 134547 0.025930
21 Kentucky Avenue 133211 0.025672
11 St. Charles Place 132361 0.025508
22 Chance 130572 0.025164
23 Indiana Avenue 128595 0.024783
12 Electric Company 127696 0.024609
24 Illinois Avenue 127213 0.024516
26 Atlantic Avenue 126095 0.024301
27 Ventnor Avenue 124093 0.023915
33 Community Chest 124060 0.023909
39 Boardwalk 123026 0.023709
31 Pacific Avenue 121587 0.023432
30 Go To Jail 120233 0.023171
32 North Carolina Avenue 119918 0.023110
14 Virginia Avenue 119751 0.023078
29 Marvin Garden 119011 0.022936
7 Chance 116277 0.022409
8 Vermont Avenue 115868 0.022330
13 States Avenue 115546 0.022268
34 Pennsylvania Avenue 115034 0.022169
4 Income Tax 114672 0.022099
9 Connecticut Avenue 114048 0.021979
6 Oriental Avenue 112772 0.021733
36 Chance 106231 0.020473
3 Baltic Avenue 105808 0.020391
2 Community Chest 104252 0.020091
1 Mediterranean Avenue 101971 0.019652
38 Luxury Tax 101806 0.019620
37 Park Place 101189 0.019501

Determine Single Property Revenue

In [18]:
prop_revenue['Low Rent'] = prop_revenue['Position'].apply(lambda x: properties[x].low_rent)
prop_revenue['High Rent'] = prop_revenue['Position'].apply(lambda x: properties[x].high_rent)

prop_revenue['Low Revenue'] = prop_revenue['Count'] * prop_revenue['Low Rent']
prop_revenue['Percent Total Low Revenue'] = prop_revenue['Low Revenue'].apply(
        lambda x: x / prop_revenue['Low Revenue'].sum()
    )
prop_revenue['High Revenue'] = prop_revenue['Count'] * prop_revenue['High Rent']
prop_revenue['Percent Total High Revenue'] = prop_revenue['High Revenue'].apply(
        lambda x: x / prop_revenue['High Revenue'].sum()
    )
In [19]:
columns = ['Property', 'Count', 'Low Rent', 'Low Revenue', 'Percent Total Low Revenue']
prop_revenue.sort('Low Revenue', ascending=False)[columns]
Out[19]:
Property Count Low Rent Low Revenue Percent Total Low Revenue
39 Boardwalk 123026 50 6151300 0.087312
25 B&O Railroad 179864 25 4496600 0.063825
28 Water Works 136729 28 3828412 0.054341
5 Reading Railroad 144861 25 3621525 0.051404
12 Electric Company 127696 28 3575488 0.050751
35 Short Line 142711 25 3567775 0.050641
15 Pennsylvania Railroad 141960 25 3549000 0.050375
37 Park Place 101189 35 3541615 0.050270
34 Pennsylvania Avenue 115034 28 3220952 0.045718
31 Pacific Avenue 121587 26 3161262 0.044871
32 North Carolina Avenue 119918 26 3117868 0.044255
29 Marvin Garden 119011 24 2856264 0.040542
26 Atlantic Avenue 126095 22 2774090 0.039376
27 Ventnor Avenue 124093 22 2730046 0.038750
24 Illinois Avenue 127213 20 2544260 0.036113
21 Kentucky Avenue 133211 18 2397798 0.034034
19 New York Avenue 146168 16 2338688 0.033195
23 Indiana Avenue 128595 18 2314710 0.032855
18 Tennessee Avenue 139522 14 1953308 0.027725
16 St. James Place 134547 14 1883658 0.026737
14 Virginia Avenue 119751 12 1437012 0.020397
11 St. Charles Place 132361 10 1323610 0.018787
13 States Avenue 115546 10 1155460 0.016401
9 Connecticut Avenue 114048 8 912384 0.012950
8 Vermont Avenue 115868 6 695208 0.009868
6 Oriental Avenue 112772 6 676632 0.009604
3 Baltic Avenue 105808 4 423232 0.006007
1 Mediterranean Avenue 101971 2 203942 0.002895
33 Community Chest 124060 0 0 0.000000
38 Luxury Tax 101806 0 0 0.000000
36 Chance 106231 0 0 0.000000
0 Go 146917 0 0 0.000000
30 Go To Jail 120233 0 0 0.000000
22 Chance 130572 0 0 0.000000
17 Community Chest 141822 0 0 0.000000
10 Jail 295199 0 0 0.000000
7 Chance 116277 0 0 0.000000
4 Income Tax 114672 0 0 0.000000
2 Community Chest 104252 0 0 0.000000
20 Free Parking 135705 0 0 0.000000
In [20]:
columns = ['Property', 'Count', 'High Rent', 'High Revenue', 'Percent Total High Revenue']
prop_revenue.sort('High Revenue', ascending=False)[columns]
Out[20]:
Property Count High Rent High Revenue Percent Total High Revenue
39 Boardwalk 123026 2000 246052000 0.087678
34 Pennsylvania Avenue 115034 1400 161047600 0.057387
31 Pacific Avenue 121587 1275 155023425 0.055241
32 North Carolina Avenue 119918 1275 152895450 0.054482
37 Park Place 101189 1500 151783500 0.054086
19 New York Avenue 146168 1000 146168000 0.052085
26 Atlantic Avenue 126095 1150 145009250 0.051672
29 Marvin Garden 119011 1200 142813200 0.050890
27 Ventnor Avenue 124093 1150 142706950 0.050852
24 Illinois Avenue 127213 1100 139934300 0.049864
21 Kentucky Avenue 133211 1050 139871550 0.049842
23 Indiana Avenue 128595 1050 135024750 0.048114
18 Tennessee Avenue 139522 950 132545900 0.047231
16 St. James Place 134547 950 127819650 0.045547
14 Virginia Avenue 119751 900 107775900 0.038405
11 St. Charles Place 132361 750 99270750 0.035374
13 States Avenue 115546 750 86659500 0.030880
9 Connecticut Avenue 114048 600 68428800 0.024384
8 Vermont Avenue 115868 550 63727400 0.022709
6 Oriental Avenue 112772 550 62024600 0.022102
25 B&O Railroad 179864 200 35972800 0.012818
3 Baltic Avenue 105808 320 33858560 0.012065
5 Reading Railroad 144861 200 28972200 0.010324
35 Short Line 142711 200 28542200 0.010171
15 Pennsylvania Railroad 141960 200 28392000 0.010117
1 Mediterranean Avenue 101971 250 25492750 0.009084
28 Water Works 136729 70 9571030 0.003411
12 Electric Company 127696 70 8938720 0.003185
33 Community Chest 124060 0 0 0.000000
36 Chance 106231 0 0 0.000000
38 Luxury Tax 101806 0 0 0.000000
0 Go 146917 0 0 0.000000
30 Go To Jail 120233 0 0 0.000000
22 Chance 130572 0 0 0.000000
17 Community Chest 141822 0 0 0.000000
10 Jail 295199 0 0 0.000000
7 Chance 116277 0 0 0.000000
4 Income Tax 114672 0 0 0.000000
2 Community Chest 104252 0 0 0.000000
20 Free Parking 135705 0 0 0.000000

Group results by color

In [21]:
color_revenue = prop_revenue.groupby( ['Color'] ).sum().reset_index()
In [22]:
color_revenue.head()
Out[22]:
Color Position Count Percent Total Count Low Rent High Rent Low Revenue Percent Total Low Revenue High Revenue Percent Total High Revenue
0 Blue 76 224215 0.043210 85 3500 9692915 0.137582 397835500 0.141764
1 Dark Purple 4 207779 0.040043 6 570 627174 0.008902 59351310 0.021149
2 Green 97 356539 0.068712 80 3950 9500082 0.134845 468966475 0.167111
3 Light Blue 23 342688 0.066043 20 1700 2284224 0.032422 194180800 0.069194
4 None 219 1637746 0.315625 0 0 0 0.000000 0 0.000000

Total hits per Color

In [23]:
columns = ['Color', 'Count', 'Percent Total Count']
color_revenue.sort('Count', ascending=False)[columns]
Out[23]:
Color Count Percent Total Count
4 None 1637746 0.315625
7 Railroad 609396 0.117442
5 Orange 420237 0.080988
8 Red 389019 0.074971
10 Yellow 369199 0.071152
6 Pink 367658 0.070855
2 Green 356539 0.068712
3 Light Blue 342688 0.066043
9 Utility 264425 0.050960
0 Blue 224215 0.043210
1 Dark Purple 207779 0.040043

Total Revenue per Color

In [24]:
columns = ['Color', 'Count', 'Low Revenue', 'Percent Total Low Revenue']
color_revenue.sort('Low Revenue', ascending=False)[columns]
Out[24]:
Color Count Low Revenue Percent Total Low Revenue
7 Railroad 609396 15234900 0.216245
0 Blue 224215 9692915 0.137582
2 Green 356539 9500082 0.134845
10 Yellow 369199 8360400 0.118668
9 Utility 264425 7403900 0.105091
8 Red 389019 7256768 0.103003
5 Orange 420237 6175654 0.087657
6 Pink 367658 3916082 0.055585
3 Light Blue 342688 2284224 0.032422
1 Dark Purple 207779 627174 0.008902
4 None 1637746 0 0.000000
In [25]:
columns = ['Color', 'Count', 'High Revenue', 'Percent Total High Revenue']
color_revenue.sort('High Revenue', ascending=False)[columns]
Out[25]:
Color Count High Revenue Percent Total High Revenue
2 Green 356539 468966475 0.167111
10 Yellow 369199 430529400 0.153414
8 Red 389019 414830600 0.147820
5 Orange 420237 406533550 0.144863
0 Blue 224215 397835500 0.141764
6 Pink 367658 293706150 0.104659
3 Light Blue 342688 194180800 0.069194
7 Railroad 609396 121879200 0.043430
1 Dark Purple 207779 59351310 0.021149
9 Utility 264425 18509750 0.006596
4 None 1637746 0 0.000000
In [ ]: