Koolaidrain

hackNY/match.py

Mar 27th, 2023
266
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 13.80 KB | None | 0 0
  1. #!/usr/bin/python3
  2.  
  3. import pandas
  4. import prettytable
  5. import queue
  6. import random
  7. import sys
  8. from typing import List, Tuple
  9.  
  10.  
  11. # misc constants
  12. MAX_INTERVIEWS_PER_REVIEWER = 5
  13. MIN_REVIEW_OVERALL_SCORE = 3.0
  14. HELP_TEXT = """
  15. I need two input files.
  16. 1. applicant_data.csv with headers: Name, Email, Interviewer, Review Overall Score: Mean, Interview Fit Score: Mean
  17. 2. interviewer_data.csv with headers: Name, Email, Confirmed?, #Committed
  18. """
  19.  
  20. # TODO: WRITE DATA TO CSVS AUTOMATICALLY?
  21.  
  22.  
  23. class Applicant:
  24. def __init__(self, series: pandas.Series):
  25. self.series = series
  26.  
  27. @property
  28. def name(self):
  29. return self.series["Name"]
  30.  
  31. @property
  32. def email(self):
  33. return self.series["Email"]
  34.  
  35. @property
  36. def reviewers(self):
  37. return self.series["Reviewers"].split("|")
  38.  
  39. @property
  40. def review_overall_score(self):
  41. return self.series["Review Overall Score: Mean"]
  42.  
  43. @property
  44. def is_candidate(self):
  45. return float(self.review_overall_score) >= MIN_REVIEW_OVERALL_SCORE
  46.  
  47. @property
  48. def interviewer(self):
  49. return self.series["Interviewer"]
  50.  
  51. @property
  52. def interview_fit_score(self):
  53. return self.series["Interview Fit Score: Mean"]
  54.  
  55.  
  56. class Interviewer:
  57. def __init__(self, series: pandas.Series):
  58. self.candidates = []
  59. self.series = series
  60.  
  61. @property
  62. def name(self):
  63. return self.series["Name"]
  64.  
  65. @property
  66. def email(self):
  67. return self.series["Email"]
  68.  
  69. @property
  70. def is_confirmed(self) -> bool:
  71. return (
  72. isinstance(self.series["Confirmed?"], str)
  73. and "Yes" in self.series["Confirmed?"]
  74. )
  75.  
  76. @property
  77. def n_committed(self) -> int:
  78. return min(int(self.series["#Committed"]), MAX_INTERVIEWS_PER_REVIEWER)
  79.  
  80. @property
  81. def n_actually_committed(self) -> int:
  82. return int(self.series["#Committed"])
  83.  
  84. @property
  85. def n_conducted(self) -> int:
  86. return int(self.series["#Conducted"])
  87.  
  88.  
  89. def register_candidates(df: pandas.DataFrame) -> List[Applicant]:
  90. candidates = []
  91. for _row_id, series in df.iterrows():
  92. applicant = Applicant(series)
  93. if applicant.is_candidate:
  94. candidates.append(applicant)
  95. print(f"- Registered {len(candidates)} candidates.")
  96. return candidates
  97.  
  98.  
  99. def register_interviewers(df: pandas.DataFrame) -> List[Interviewer]:
  100. interviewers = []
  101. for _row_id, series in df.iterrows():
  102. interviewer = Interviewer(series)
  103. if interviewer.is_confirmed:
  104. interviewers.append(interviewer)
  105. print(f"- Registered {len(interviewers)} interviewers.")
  106. return interviewers
  107.  
  108.  
  109. def summarize_interviewer_progress(interviewers: List[Interviewer]):
  110. interviewers = sorted(interviewers, key=lambda i: i.n_actually_committed, reverse=True)
  111. table = prettytable.PrettyTable(["Name", "# Conducted", "# Matches", "# Committed"])
  112. for interviewer in interviewers:
  113. row = [
  114. interviewer.name,
  115. interviewer.n_conducted,
  116. len(interviewer.candidates),
  117. interviewer.n_actually_committed,
  118. ]
  119. table.add_row(row)
  120. print(table)
  121.  
  122.  
  123. def generate_new_matches(
  124. todo_candidates: List[Applicant], interviewers: List[Interviewer]
  125. ) -> List[Tuple[Applicant, Interviewer]]:
  126. new_matches = []
  127. pq = queue.PriorityQueue()
  128. random.shuffle(todo_candidates)
  129. random.shuffle(interviewers)
  130. interviewer_index = {i.name: i for i in interviewers}
  131. for interviewer in interviewers:
  132. priority = get_priority(interviewer)
  133. pq.put((priority, interviewer.name))
  134. # initially, distribute based on interviewer capacity
  135. while todo_candidates:
  136. candidate = todo_candidates.pop()
  137. priority, interviewer_name = pq.get()
  138. interviewer = interviewer_index[interviewer_name]
  139. interviewer.candidates.append(candidate)
  140. new_matches.append((candidate, interviewer))
  141. new_priority = get_priority(interviewer)
  142. pq.put((new_priority, interviewer_name))
  143. # randomly redistribute if needed
  144. for i in range(len(new_matches)):
  145. candidate, interviewer = new_matches[i]
  146. if interviewer.name in candidate.reviewers:
  147. new_matches.pop(i)
  148. interviewer.candidates.pop(interviewer.candidates.index(candidate))
  149. valid_interviewers = [
  150. i for i in interviewers
  151. if i.name not in candidate.reviewers and len(i.candidates) < i.n_committed
  152. ]
  153. new_matches.append((candidate, random.choice(valid_interviewers)))
  154. return new_matches
  155.  
  156.  
  157. # this function outputs something comparable
  158. def get_priority(interviewer: Interviewer):
  159. n_existing_matches = len(interviewer.candidates)
  160. remaining_capacity = interviewer.n_committed - n_existing_matches
  161. candidate_saturation_ratio = n_existing_matches / interviewer.n_committed
  162. # apply a positive when you want the number to lean against new matches
  163. # apply a negative when you want the number to lean toward new matches
  164. return (candidate_saturation_ratio, n_existing_matches, -1 * remaining_capacity)
  165.  
  166.  
  167. def main(applicant_df: pandas.DataFrame, interviewer_df: pandas.DataFrame):
  168. print("\nRegistering structs...")
  169. n_existing_matches = 0
  170. todo_candidates = []
  171. candidates = register_candidates(applicant_df)
  172. interviewers = register_interviewers(interviewer_df)
  173. interviewer_index = {i.name: i for i in interviewers}
  174. for candidate in candidates:
  175. if isinstance(candidate.interviewer, str):
  176. interviewer = interviewer_index[candidate.interviewer]
  177. interviewer.candidates.append(candidate.name)
  178. n_existing_matches += 1
  179. else:
  180. todo_candidates.append(candidate)
  181. print(f"- Registered {n_existing_matches} existing candidate::interviewer matches.")
  182. print("\nHere is a summary of current interviewer progress.")
  183. summarize_interviewer_progress(interviewers)
  184. if not todo_candidates:
  185. print("\nNo candidates need matches. Exiting.")
  186. return
  187. print(f"\nGenerating {len(todo_candidates)} matches...")
  188. new_matches = generate_new_matches(todo_candidates, interviewers)
  189. print("\nHere are the new matches. Assign them in Embark and email the relevant parties.")
  190. for candidate, interviewer in new_matches:
  191. print(f"- {candidate.name}: {interviewer.name}")
  192. print("\nHere is a summary of future interviewer progress (after the new matches).")
  193. summarize_interviewer_progress(interviewers)
  194. print("\nComplete.")
  195.  
  196.  
  197. if __name__ == "__main__":
  198. if len(sys.argv) < 3:
  199. print(HELP_TEXT)
  200. sys.exit(1)
  201. applicant_df = pandas.read_csv(sys.argv[1])
  202. interviewer_df = pandas.read_csv(sys.argv[2])
  203. main(applicant_df, interviewer_df)
  204.  
  205. ~/hackny/admissions/match
  206. ❯ cat match_interviewers.py
  207. #!/usr/bin/python3
  208.  
  209. import pandas
  210. import prettytable
  211. import queue
  212. import random
  213. import sys
  214. from typing import List, Tuple
  215.  
  216.  
  217. # misc constants
  218. MAX_INTERVIEWS_PER_REVIEWER = 5
  219. MIN_REVIEW_OVERALL_SCORE = 3.0
  220. HELP_TEXT = """
  221. I need two input files.
  222. 1. applicant_data.csv with headers: Name, Email, Interviewer, Review Overall Score: Mean, Interview Fit Score: Mean
  223. 2. interviewer_data.csv with headers: Name, Email, Confirmed?, #Committed
  224. """
  225.  
  226. # TODO: WRITE DATA TO CSVS AUTOMATICALLY?
  227.  
  228.  
  229. class Applicant:
  230. def __init__(self, series: pandas.Series):
  231. self.series = series
  232.  
  233. @property
  234. def name(self):
  235. return self.series["Name"]
  236.  
  237. @property
  238. def email(self):
  239. return self.series["Email"]
  240.  
  241. @property
  242. def reviewers(self):
  243. return self.series["Reviewers"].split("|")
  244.  
  245. @property
  246. def review_overall_score(self):
  247. return self.series["Review Overall Score: Mean"]
  248.  
  249. @property
  250. def is_candidate(self):
  251. return float(self.review_overall_score) >= MIN_REVIEW_OVERALL_SCORE
  252.  
  253. @property
  254. def interviewer(self):
  255. return self.series["Interviewer"]
  256.  
  257. @property
  258. def interview_fit_score(self):
  259. return self.series["Interview Fit Score: Mean"]
  260.  
  261.  
  262. class Interviewer:
  263. def __init__(self, series: pandas.Series):
  264. self.candidates = []
  265. self.series = series
  266.  
  267. @property
  268. def name(self):
  269. return self.series["Name"]
  270.  
  271. @property
  272. def email(self):
  273. return self.series["Email"]
  274.  
  275. @property
  276. def is_confirmed(self) -> bool:
  277. return (
  278. isinstance(self.series["Confirmed?"], str)
  279. and "Yes" in self.series["Confirmed?"]
  280. )
  281.  
  282. @property
  283. def n_committed(self) -> int:
  284. return min(int(self.series["#Committed"]), MAX_INTERVIEWS_PER_REVIEWER)
  285.  
  286. @property
  287. def n_actually_committed(self) -> int:
  288. return int(self.series["#Committed"])
  289.  
  290. @property
  291. def n_conducted(self) -> int:
  292. return int(self.series["#Conducted"])
  293.  
  294.  
  295. def register_candidates(df: pandas.DataFrame) -> List[Applicant]:
  296. candidates = []
  297. for _row_id, series in df.iterrows():
  298. applicant = Applicant(series)
  299. if applicant.is_candidate:
  300. candidates.append(applicant)
  301. print(f"- Registered {len(candidates)} candidates.")
  302. return candidates
  303.  
  304.  
  305. def register_interviewers(df: pandas.DataFrame) -> List[Interviewer]:
  306. interviewers = []
  307. for _row_id, series in df.iterrows():
  308. interviewer = Interviewer(series)
  309. if interviewer.is_confirmed:
  310. interviewers.append(interviewer)
  311. print(f"- Registered {len(interviewers)} interviewers.")
  312. return interviewers
  313.  
  314.  
  315. def summarize_interviewer_progress(interviewers: List[Interviewer]):
  316. interviewers = sorted(interviewers, key=lambda i: i.n_actually_committed, reverse=True)
  317. table = prettytable.PrettyTable(["Name", "# Conducted", "# Matches", "# Committed"])
  318. for interviewer in interviewers:
  319. row = [
  320. interviewer.name,
  321. interviewer.n_conducted,
  322. len(interviewer.candidates),
  323. interviewer.n_actually_committed,
  324. ]
  325. table.add_row(row)
  326. print(table)
  327.  
  328.  
  329. def generate_new_matches(
  330. todo_candidates: List[Applicant], interviewers: List[Interviewer]
  331. ) -> List[Tuple[Applicant, Interviewer]]:
  332. new_matches = []
  333. pq = queue.PriorityQueue()
  334. random.shuffle(todo_candidates)
  335. random.shuffle(interviewers)
  336. interviewer_index = {i.name: i for i in interviewers}
  337. for interviewer in interviewers:
  338. priority = get_priority(interviewer)
  339. pq.put((priority, interviewer.name))
  340. # initially, distribute based on interviewer capacity
  341. while todo_candidates:
  342. candidate = todo_candidates.pop()
  343. priority, interviewer_name = pq.get()
  344. interviewer = interviewer_index[interviewer_name]
  345. interviewer.candidates.append(candidate)
  346. new_matches.append((candidate, interviewer))
  347. new_priority = get_priority(interviewer)
  348. pq.put((new_priority, interviewer_name))
  349. # randomly redistribute if needed
  350. for i in range(len(new_matches)):
  351. candidate, interviewer = new_matches[i]
  352. if interviewer.name in candidate.reviewers:
  353. new_matches.pop(i)
  354. interviewer.candidates.pop(interviewer.candidates.index(candidate))
  355. valid_interviewers = [
  356. i for i in interviewers
  357. if i.name not in candidate.reviewers and len(i.candidates) < i.n_committed
  358. ]
  359. new_matches.append((candidate, random.choice(valid_interviewers)))
  360. return new_matches
  361.  
  362.  
  363. # this function outputs something comparable
  364. def get_priority(interviewer: Interviewer):
  365. n_existing_matches = len(interviewer.candidates)
  366. remaining_capacity = interviewer.n_committed - n_existing_matches
  367. candidate_saturation_ratio = n_existing_matches / interviewer.n_committed
  368. # apply a positive when you want the number to lean against new matches
  369. # apply a negative when you want the number to lean toward new matches
  370. return (candidate_saturation_ratio, n_existing_matches, -1 * remaining_capacity)
  371.  
  372.  
  373. def main(applicant_df: pandas.DataFrame, interviewer_df: pandas.DataFrame):
  374. print("\nRegistering structs...")
  375. n_existing_matches = 0
  376. todo_candidates = []
  377. candidates = register_candidates(applicant_df)
  378. interviewers = register_interviewers(interviewer_df)
  379. interviewer_index = {i.name: i for i in interviewers}
  380. for candidate in candidates:
  381. if isinstance(candidate.interviewer, str):
  382. interviewer = interviewer_index[candidate.interviewer]
  383. interviewer.candidates.append(candidate.name)
  384. n_existing_matches += 1
  385. else:
  386. todo_candidates.append(candidate)
  387. print(f"- Registered {n_existing_matches} existing candidate::interviewer matches.")
  388. print("\nHere is a summary of current interviewer progress.")
  389. summarize_interviewer_progress(interviewers)
  390. if not todo_candidates:
  391. print("\nNo candidates need matches. Exiting.")
  392. return
  393. print(f"\nGenerating {len(todo_candidates)} matches...")
  394. new_matches = generate_new_matches(todo_candidates, interviewers)
  395. print("\nHere are the new matches. Assign them in Embark and email the relevant parties.")
  396. for candidate, interviewer in new_matches:
  397. print(f"- {candidate.name}: {interviewer.name}")
  398. print("\nHere is a summary of future interviewer progress (after the new matches).")
  399. summarize_interviewer_progress(interviewers)
  400. print("\nComplete.")
  401.  
  402.  
  403. if __name__ == "__main__":
  404. if len(sys.argv) < 3:
  405. print(HELP_TEXT)
  406. sys.exit(1)
  407. applicant_df = pandas.read_csv(sys.argv[1])
  408. interviewer_df = pandas.read_csv(sys.argv[2])
  409. main(applicant_df, interviewer_df)
Advertisement
Add Comment
Please, Sign In to add comment