Coverage for algorithm/matching.py: 100%
44 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-05 14:02 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-05 14:02 +0000
1"""
2The matching algorithm is a stable marriage algorithm that matches students
3to placements based on their preferences. The algorithm is implemented in the
4`Matching` class. The `find_best_match` method finds the best match for students and placements.
5"""
7from typing import List, Dict, Tuple
10class Matching:
11 """Class to match students to placements based on their preferences."""
13 def __init__(
14 self,
15 student_rank: Dict[str, List[str]],
16 placement_rank: Dict[str, Dict[str, int]],
17 ):
18 # Students' preferences
19 self.student_rank = student_rank
20 # Employers' rankings, includes "positions" key
21 self.placement_rank = placement_rank
22 # To store the final matches
23 self.potential_match: Dict[str, List[str]] = {}
24 # To store the final matches (Unmatched, Matched)
25 self.final_result: Tuple[List[str], Dict[str, List[str]]] = ([], {})
27 def find_best_match(self) -> Tuple[List[str], Dict[str, List[str]]]:
28 """Find the best match for students and placements."""
29 students = list(self.student_rank.keys()) # List of unmatched students
30 unmapped = [] # List of students who cannot be matched (no more preferences)
31 # Keep track of employers' current engagements
32 placement_current_match: Dict[str, List[str]] = {
33 placement: [] for placement in self.placement_rank.keys()
34 }
36 # print("Initial state:")
37 # print(f"Students: {students}")
38 # print(f"Employers' current matches: {placement_current_match}")
40 while students:
41 student = students.pop(0) # Pick the next unmatched student
42 # print(f"\nProcessing student: {student}")
44 if not self.student_rank[student]: # If student has no more preferences
45 # print(f"Student {student} has no more preferences and is unmapped.")
46 unmapped.append(student)
47 continue
49 choice = self.student_rank[student].pop(0)
50 # print(f"{student} prefers placement: {choice}")
52 # Check current matches of the chosen employer
53 current_matches = placement_current_match[choice]
54 positions = int(self.placement_rank[choice]["positions"])
56 # If there are positions available, add the student
57 if (
58 len(current_matches) < positions
59 and student in self.placement_rank[choice]
60 ):
61 # print(f"{choice} has available positions. Adding {student}.")
62 placement_current_match[choice].append(student)
63 self.potential_match[choice] = placement_current_match[choice]
65 else:
66 # print(f"{choice} is full or {student} is not ranked by the employer.")
67 if current_matches:
68 # print(f"Current matches at {choice}: {current_matches}")
70 # Find the weakest match using the employer's ranking
71 weakest_match = current_matches[-1] # Get the least preferred match
72 weakest_match_rank = self.placement_rank[choice][weakest_match]
73 index = len(current_matches) - 1
75 for match in current_matches:
76 if self.placement_rank[choice][match] > weakest_match_rank:
77 weakest_match = match
78 weakest_match_rank = self.placement_rank[choice][match]
79 index = current_matches.index(weakest_match)
81 # Check if the student is in the employer's ranking
82 if student in self.placement_rank[choice]:
83 new_candidate_rank = self.placement_rank[choice][student]
84 # print(
85 # f"Comparing {student} (rank {new_candidate_rank}) with
86 # weakest match {weakest_match} (rank {weakest_match_rank})."
87 # )
89 # If new student is ranked higher (lower number), replace the weakest match
90 if new_candidate_rank < weakest_match_rank:
91 # print(
92 # f"{student} is ranked higher than {weakest_match}. Replacing."
93 # )
94 placement_current_match[choice][
95 index
96 ] = student # Replace weakest match
97 self.potential_match[choice] = placement_current_match[
98 choice
99 ]
100 students.insert(
101 0, weakest_match
102 ) # Re-add the replaced student to the unmatched list
103 else:
104 # print(
105 # f"{student} is ranked lower than {weakest_match}. Rejected."
106 # )
107 students.insert(
108 0, student
109 ) # Add student back to the unmatched list
110 else:
111 # print(f"{student} is not ranked by {choice}. Rejected.")
112 students.insert(
113 0, student
114 ) # Re-add student to the unmatched list
115 else:
116 # print(
117 # f"{choice} has no current matches. {student} has not been ranked by {choice}."
118 # )
119 students.insert(0, student)
121 self.final_result = (unmapped, self.potential_match)
122 # print("\nFinal result:")
123 # print(f"Unmapped students: {self.final_result[0]}")
124 # print(f"Matched students: {self.final_result[1]}")
125 return self.final_result
127 def get_matches(self) -> Tuple[List[str], Dict[str, List[str]]]:
128 """Get final result."""
129 # print("\nReturning final matches:")
130 # print(f"Unmapped: {self.final_result[0]}")
131 # print(f"Matched: {self.final_result[1]}")
132 return self.final_result