Last active
August 29, 2015 14:25
-
-
Save shidarin/d3c476fe53f8bc998208 to your computer and use it in GitHub Desktop.
Script used to read a quiz results for /r/TheExpanse in CSV and grade, then select winners.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python | |
"""Script used to read a quiz results CSV and grade, then select winners.""" | |
#============================================================================== | |
# IMPORTS | |
#============================================================================== | |
import csv | |
from datetime import datetime | |
import random | |
import sys | |
#============================================================================== | |
# GLOBALS | |
#============================================================================== | |
ANSWER_KEY = { | |
# Page numbers reference First Edition, 2011 | |
'author_1': 'Ty Franck', | |
'author_2': 'Daniel Abraham', | |
'honor_among_thieves': [ | |
'Star Wars books always get New York Times Bestseller', # Signing Q&A | |
"It's friggin Star Wars" # It is known. | |
], | |
'ty_job': 'Assistant to George RR Martin', | |
'daniel_middle_name': 'James', # Wikipedia | |
'julie_mao_prison': 'The Anubis', # This is the question JASC corrected. | |
'protogen_slogan': 'First Fastest Furthest.', # p342 | |
'holden_ranch': 'Montana, Earth', | |
'rocinante': "The name of Don Quixote's horse", # Wikipedia | |
'cant_capt': 'McDowell', # p15 | |
'flophouse_name': 'Lionel Polanski', # p220 | |
'proto_growth': 'Radiation', # p255 | |
'eros_security': 'Carne Por la Machina', # p228 | |
'thothe': 'Dresdon', # p410 | |
'UNN_ship': 'Ravi', # p549 | |
'charges': [ # p549 | |
'Interfering with UNN military operations', | |
'Unlawfully commandeering UNN military assets' | |
] | |
} | |
#============================================================================== | |
# CLASSES | |
#============================================================================== | |
class Entry(object): | |
"""A single entry in the contest.""" | |
entries = {} | |
def __init__(self, entry): | |
"""Initialize the entry with all answers, using a dictionary. | |
Sample `entry` (not all correct answers) is as follows:: | |
{'Who is the new security firm on Eros?': | |
'Carne Por la Machina', | |
'The UNN ship chasing Eros with the Roci is named what?': | |
'Ravi', | |
'In Don Quixote, Rocinante is...': | |
"The name of Don Quixote's horse", | |
'James SA Corey wrote Star Wars: Honor Among Thieves because...': | |
"It's friggin Star Wars", | |
'Julie Mao is held prisoner aboard what ship?': | |
'Unnamed stealth ship', | |
'If your preferred prize is unavailable, | |
do you want the other prize?': | |
'Yes', | |
'Timestamp': | |
'7/14/2015 14:01:28', | |
"The captain of the Canterbury's name is...": | |
'McDowell', | |
'Are you over 18?': | |
'Yes', | |
'What makes the protomolecule grow?': | |
'Radiation', | |
'Who runs Thothe station?': | |
'Dresdon', | |
"Protogen's secret slogan is...": | |
'First Fastest Furthest.', | |
"James Holden's family lives where?": | |
'Montana, Earth', | |
'Who is Author #1?': | |
'Ty Franck', | |
"What's your reddit username?": | |
'backstept', | |
'Which do you prefer- Drive or google cardboard?': | |
'Drive', | |
'Who is Author #2?': | |
'Daniel Abraham', | |
"Daniel Abraham's middle name is...": | |
'Corey', | |
"One of Ty Franck's previous jobs was...": | |
'Assistant to George RR Martin', | |
'The name Julie Mao checks in to a flophouse on Eros Under is...': | |
'Lionel Polanski', | |
'What does the captain of the above ship charge Holden with?': | |
'Unlawful occupation of MCRN military assets'} | |
""" | |
self._username = entry[ | |
"What's your reddit username?" | |
].replace('/u/', '') | |
if self._username in self.entries: | |
raise ValueError( | |
"User {0} has entered more than once! Only the first will " | |
"be counted.".format( | |
self._username | |
)) | |
if self._username == 'shidarin': | |
raise ValueError("Shidarin is not allowed to enter.") | |
self._adult = entry['Are you over 18?'] | |
if self._adult == 'No': | |
raise ValueError("User {0} is not over 18!".format(self._username)) | |
self._time = self._convert_time(entry['Timestamp']) | |
if self._time > datetime(month=7, day=20, year=2015): | |
raise ValueError("User {0} entered after contest was over!") | |
self._author_1 = entry['Who is Author #1?'] | |
self._author_2 = entry['Who is Author #2?'] | |
self._honor_among_thieves = entry[ | |
'James SA Corey wrote Star Wars: Honor Among Thieves because...' | |
] | |
self._ty_job = entry["One of Ty Franck's previous jobs was..."] | |
self._daniel_middle_name = entry["Daniel Abraham's middle name is..."] | |
self._julie_mao_prison = entry[ | |
'Julie Mao is held prisoner aboard what ship?' | |
] | |
self._protogen_slogan = entry["Protogen's secret slogan is..."] | |
self._holden_ranch = entry["James Holden's family lives where?"] | |
self._rocinante = entry['In Don Quixote, Rocinante is...'] | |
self._cant_capt = entry["The captain of the Canterbury's name is..."] | |
self._flophouse_name = entry[ | |
'The name Julie Mao checks in to a flophouse on Eros Under is...' | |
] | |
self._proto_growth = entry['What makes the protomolecule grow?'] | |
self._eros_security = entry['Who is the new security firm on Eros?'] | |
self._thothe = entry['Who runs Thothe station?'] | |
self._UNN_ship = entry[ | |
'The UNN ship chasing Eros with the Roci is named what?' | |
] | |
self._charges = entry[ | |
'What does the captain of the above ship charge Holden with?' | |
].split(', ') | |
self._prize_preference = entry[ | |
'Which do you prefer- Drive or google cardboard?' | |
] | |
self._preferred_prize_fallback = entry[ | |
'If your preferred prize is unavailable, ' | |
'do you want the other prize?' | |
] | |
self._score = None | |
self.entries[self._username] = self | |
# Special Methods | |
def __repr__(self): | |
return "<{cls}: {username}>".format( | |
cls=self.__class__.__name__, | |
username=self.username | |
) | |
# Properties | |
@property | |
def adult(self): | |
"""We raise a value error during init if this is false, but...""" | |
return True if self._adult == 'Yes' else False | |
@property | |
def preferred_prize_fallback(self): | |
return True if self._preferred_prize_fallback == 'Yes' else False | |
@property | |
def prize_preference(self): | |
return self._prize_preference | |
@property | |
def score(self): | |
if not self._score: | |
self._score = self._score_entry() | |
return self._score | |
@property | |
def time(self): | |
return self._time | |
@property | |
def username(self): | |
return self._username | |
# Private Methods | |
@staticmethod | |
def _convert_time(timestamp): | |
"""Convert a timestamp string to datetime object. | |
Sample timestamp: '7/14/2015 14:01:28' | |
""" | |
return datetime.strptime(timestamp, '%m/%d/%Y %H:%M:%S') | |
def _score_entry(self): | |
"""Score the entry according to the answer key.""" | |
score = 0 | |
score += self._score_single(self._author_1, 'author_1') | |
score += self._score_single(self._author_2, 'author_2') | |
score += self._score_single( | |
self._honor_among_thieves, 'honor_among_thieves' | |
) | |
score += self._score_single(self._ty_job, 'ty_job') | |
score += self._score_single( | |
self._daniel_middle_name, 'daniel_middle_name' | |
) | |
if self.time > datetime(month=7, day=14, year=2015, hour=16): | |
score += self._score_single( | |
self._julie_mao_prison, 'julie_mao_prison' | |
) | |
else: | |
# People who entered the contest before the datetime above did not | |
# actually have a correct answer to this question, so all | |
# will be credited. | |
score += 1 | |
score += self._score_single(self._protogen_slogan, 'protogen_slogan') | |
score += self._score_single(self._holden_ranch, 'holden_ranch') | |
score += self._score_single(self._rocinante, "rocinante") | |
score += self._score_single(self._cant_capt, 'cant_capt') | |
score += self._score_single(self._flophouse_name, 'flophouse_name') | |
score += self._score_single(self._proto_growth, 'proto_growth') | |
score += self._score_single(self._eros_security, 'eros_security') | |
score += self._score_single(self._thothe, 'thothe') | |
score += self._score_single(self._UNN_ship, 'UNN_ship') | |
score += self._score_checkbox(self._charges, 'charges') | |
return score | |
@staticmethod | |
def _score_single(answer, key): | |
"""Score a single answer against the ANSWER_KEY.""" | |
return 1 if answer in ANSWER_KEY[key] else 0 | |
@staticmethod | |
def _score_checkbox(answer, key): | |
"""Score a multiple possible answer against the ANSWER_KEY.""" | |
score = 0 | |
for ans in answer: | |
if ans in ANSWER_KEY[key]: | |
score += 0.5 | |
return score | |
#============================================================================== | |
# PRIVATE METHODS | |
#============================================================================== | |
def _print_winner_line(winner): | |
print "{user}: {score} - {preferred}.{sub}".format( | |
user=winner.username, | |
score=winner.score, | |
preferred=winner.prize_preference, | |
sub=' No substitutions' if not winner.preferred_prize_fallback else '' | |
) | |
#============================================================================== | |
# MAIN | |
#============================================================================== | |
def main(): | |
"""Main script.""" | |
csv_file = sys.argv[1] | |
with open(csv_file, 'r') as f: | |
rows = csv.DictReader(f) | |
for entry in rows: | |
try: | |
Entry(entry) | |
except ValueError as err: | |
print "Invalid entry:", err | |
print "\nReceived the following entries and scores:" | |
entries = sorted( | |
Entry.entries.values(), key=lambda x: x.score, reverse=True | |
) | |
for entry in entries: | |
print entry.username, ':', entry.score | |
top_50_percent = entries[:len(entries)/2] # Grab the top half of entries | |
low_score = top_50_percent[-1].score # Lowest score is last entry. | |
for entry in entries: | |
# We need to make sure we include all entries that tie with the | |
# lowest scoring entry in the cutoff. | |
if entry.score == low_score and entry not in top_50_percent: | |
top_50_percent.append(entry) | |
print "\nCut off score for top 50 percent is {score}.".format( | |
score=low_score | |
) | |
winners = [] | |
while len(winners) < 10: | |
# Keep randomly selecting winners until we have 10. | |
winner = random.choice(top_50_percent) | |
if winner not in winners: | |
winners.append(winner) | |
winners = sorted(winners, key=lambda x: x.score, reverse=True) | |
for winner in winners: | |
# Remove so they are not included in fallback calculation | |
top_50_percent.remove(winner) | |
_print_winner_line(winner) | |
print '\nFallbacks are:' | |
# The top 5 scoring entries who were not randomly selected will be used as | |
# fallbacks in the order of their scoring. | |
for fallback in top_50_percent[:5]: | |
_print_winner_line(fallback) | |
# At this point, the scorer should be able to easily figure out who gets | |
# what prize. We could go a step further and have the program do it, | |
# but why over engineer this more than it already is? | |
#============================================================================== | |
# GATE | |
#============================================================================== | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment