/
lottery.py
executable file
·209 lines (168 loc) · 7.51 KB
/
lottery.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
#!/usr/bin/python
# A script that executes the Newton GWC Registration lottery. The input is
# a (cleaned -- see clean_input.py) CSV file representing the lottery
# entries. It runs the lottery on first time and/or returning students,
# assigning to two sections of size N each.
#
# It runs the following algorithm until 2*N entrants are assigned:
# * draw the next entrant
#
# * if entrant can go to only one section, assign them to that section if it is still open.
#
# * else, if entrant can go to either section, defer making an assignment until
# one section fills, then assign that entrant to the other section.
#
# If neither section fills up but the limit of 2*N entrants is reached
# (including both assignments and deferred decisions), output the 3 lists:
# assigned-A, assigned-B, flexible. The let's the staff make the final
# assignments of flexible entrants to optimize age distribution and special
# cases (e.g. siblings assigned to different/same section)
#
# If either section fills up, all the deferrals are assigned to the
# remaining open section before the next entrant is drawn. When the global
# limit is reached, output 2 lists: assigned-A and assigned-B.
#
# The entrants are drawn randomly by shuffling the (timestamp-ordered)
# input using Python's random.shuffle() routine, and a random seed. The
# random seed can be provided at the command line (for reproducibility /
# testing) or can be taken from the system.
#
# Usage: lottery.py --section_limit N [--returning | --firsttime] --seed R --response_filename <filename>
#
# Author: David Miller, 2016
import csv
import random
import sys
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--section_limit', type=int)
parser.add_argument('--seed', help='Seed to random generator. If not set, users system seed', type=int)
parser.add_argument('--returning', help='Specify this to include returning participants', action='store_true')
parser.add_argument('--firsttime', help='Specify this to include first-time participants', action='store_true')
parser.add_argument('--response_filename', help='Name of the CSV file containing the lottery entries')
args = parser.parse_args()
FLAGS = vars(args)
# print "limit: %d" % FLAGS['section_limit']
# print "returning: %r" % FLAGS['returning']
# print "first-time: %r" % FLAGS['firsttime']
section_limit = FLAGS['section_limit']
# response_filename = '/Users/drm/Downloads/Lottery sample input - Sheet1.csv'
# response_filename = '/Users/drm/Downloads/Newton Girls Who Code Club 2016-2017 Registration (Responses) - Form Responses 1.csv'
response_filename = FLAGS['response_filename']
csv_file = open(response_filename, 'rb')
reader = csv.DictReader(csv_file)
# Simple accessor methods for lottery entries
def first_name(entry):
return entry["Child's first name"]
def last_name(entry):
return entry["Child's last name"]
def available_tuesday(entry):
response = entry["Which sections are you entering the lottery for (you'll be assigned to at most one)?"]
return 'T' in response
def available_friday(entry):
response = entry["Which sections are you entering the lottery for (you'll be assigned to at most one)?"]
return 'F' in response
def returning_student(entry):
response = entry["Are you a returning Newton GWC Club member?"]
return response == 'Yes'
def available_both_days(entry):
return available_tuesday(entry) and available_friday(entry)
entries = []
for entry in reader:
if ((returning_student(entry) and FLAGS['returning']) or
(not returning_student(entry) and FLAGS['firsttime'])):
entries.append(entry)
if FLAGS['seed']:
random.seed(FLAGS['seed'])
random.shuffle(entries)
accepted_tuesday = []
accepted_friday = []
accepted_flexible = []
drawn_after_full = []
def section_open(accepted_list):
return len(accepted_list) < section_limit
def total_accepted():
return len(accepted_tuesday) + len(accepted_friday) + len(accepted_flexible)
def to_string(entry):
return ' '.join([first_name(entry), last_name(entry)])
def print_entry_list(entry_list):
for entry in entry_list:
print to_string(entry)
def print_lottery_result():
print "\nAll done! Here are the results."
print "\nAccepted Tuesday (%d): " % len(accepted_tuesday)
print_entry_list(accepted_tuesday)
print "\nAccepted Friday (%d): " % len(accepted_friday)
print_entry_list(accepted_friday)
print "\nAccepted flexible (%d): " % len(accepted_flexible)
print_entry_list(accepted_flexible)
print "\nDrawn after full (%d): " % len(drawn_after_full)
print_entry_list(drawn_after_full)
print "\nNever drawn (%d): " % len(entries)
print_entry_list(entries)
# Phase 1 -- put entrants in their sole-available section, or accept them but be flexible if they are.
print "\nStarting Phase 1"
while (section_open(accepted_tuesday) and section_open(
accepted_friday) and total_accepted() < 2 * section_limit and entries):
entry = entries.pop()
print "\nDrew entry: %s" % to_string(entry)
if available_both_days(entry):
accepted_flexible.append(entry)
print "Keeping, flexible."
elif available_friday(entry):
accepted_friday.append(entry)
print "Assigned to Friday"
elif available_tuesday(entry):
accepted_tuesday.append(entry)
print "Assigned to Tuesday"
else:
print "Error! Entry %s had no valid availability" % to_string(entry)
# If we have enough flexibility that neither section was
# individually over-subscribed, print the three lists to allow Newton
# GWC Staff to distribute the flexible students to make a good balance
# of ages in each section. We're done.
if total_accepted() == 2 * section_limit or not entries:
print_lottery_result()
sys.exit()
print "\nStarting Phase 2"
# Phase 2 -- one section filled up with entrants who have no
# flexibility. Move all the flexible ones to the other section.
underfull_section = None
if section_open(accepted_tuesday) and not section_open(accepted_friday):
print "Friday section filled up! Assigning flexibles to Tuesday."
accepted_tuesday.extend(accepted_flexible)
accepted_flexible = []
underfull_section = "tuesday"
elif section_open(accepted_friday) and not section_open(accepted_tuesday):
print "Tuesday section filled up! Assigning flexibles to Friday."
accepted_friday.extend(accepted_flexible)
accepted_flexible = []
underfull_section = "friday"
else:
print "Error! Exited initial phase of the lottery without filling all slots and without filling either section."
print_lottery_result()
sys.exit()
# Phase 3 fill it out remaining section with additional draws from the pool.
print "\nStarting Phase 3"
while total_accepted() < 2 * section_limit and entries:
entry = entries.pop()
print "\nDrew entry: %s" % to_string(entry)
if available_both_days(entry):
if underfull_section == "tuesday":
accepted_tuesday.append(entry)
print "Assigned to Tuesday"
elif underfull_section == "friday":
accepted_friday.append(entry)
print "Assigned to Friday"
else:
print "Error! Invalid value for underfull section"
elif available_friday(entry) and underfull_section == "friday":
accepted_friday.append(entry)
print "Assigned to Friday"
elif available_tuesday(entry) and underfull_section == "tuesday":
accepted_tuesday.append(entry)
print "Assigned to Tuesday"
else:
drawn_after_full.append(entry)
print "Can't assign! Only available for full section"
print_lottery_result()