import urllib
import yaml
def read_yaml_to_dict(where, filename):
"""Read in the YAML data from the URL."""
url = "/".join((where, filename))
with urllib.request.urlopen(url) as response:
dictionary = yaml.safe_load(response.read())
return dictionary
base_url = "https://zenodo.org/record/3688091/files"
resident_preferences = read_yaml_to_dict(base_url, "residents.yml")
hospital_preferences = read_yaml_to_dict(base_url, "hospitals.yml")
hospital_capacities = read_yaml_to_dict(base_url, "capacities.yml")Allocating university medics to hospital placements
For this tutorial, we will be using HR to solve a real-world problem.
Imagine that we represent a centralised body responsible for assigning newly qualified doctors to their hospital posts. This job is already done by computers around the world using software like matching, and now we can do it, too.
The hospital capacities and all of the preferences for this instance are entirely fabricated but the hospitals are some of those from the South Wales area of the UK.
Collecting and reading the data
The data for this tutorial have been archived on Zenodo. The source code used to generate them is here.
We can load in the data as Python dictionaries with the urllib and PyYAML libraries.
As it turns out, this game is fairly large. There are 200 medics (or residents) applying to 7 hospitals with a total of 210 spaces available:
num_residents = len(resident_preferences)
num_hospitals = len(hospital_preferences)
total_spaces = sum(hospital_capacities.values())
num_residents, num_hospitals, total_spaces(200, 7, 210)
Creating the players
With the data read in, we can create the players for our game.
We don’t need to worry about cleaning the data as they were created to form a valid game instance.
This particular instance is not only too large to be done by hand, but we also won’t be creating the players manually. Instead, we will use the HospitalResident.create_from_dictionaries() method.
from matching.games import HospitalResident
game = HospitalResident.create_from_dictionaries(
resident_preferences, hospital_preferences, hospital_capacities
)Running the game
Now, we have a complete game instance to solve.
We have the option to find a resident- or hospital-optimal solution. In this case, as is often done in reality, we will be using the former.
solution = game.solve(optimal="resident")Checking the matching
The solution is a dictionary-like object with hospitals as keys and lists of their matched residents as values.
for hospital, residents in solution.items():
print(f"{hospital} ({len(residents)} / {hospital.capacity}): {residents}")Dewi Sant (30 / 30): [067, 022, 023, 158, 139, 065, 160, 131, 011, 137, 039, 045, 013, 046, 072, 037, 086, 152, 144, 154, 130, 040, 010, 159, 083, 019, 169, 193, 168, 079]
Prince Charles (29 / 30): [027, 133, 106, 081, 051, 044, 069, 157, 110, 119, 129, 107, 135, 034, 007, 194, 198, 061, 087, 041, 183, 136, 059, 178, 009, 008, 031, 070, 026]
Prince of Wales (30 / 30): [143, 128, 048, 175, 078, 132, 151, 030, 124, 138, 088, 004, 199, 173, 017, 097, 064, 025, 112, 181, 171, 196, 111, 035, 185, 156, 140, 001, 197, 177]
Royal Glamorgan (30 / 30): [073, 118, 096, 089, 014, 126, 142, 053, 021, 018, 104, 015, 147, 153, 033, 113, 146, 076, 123, 042, 117, 024, 029, 000, 016, 134, 058, 166, 075, 174]
Royal Gwent (27 / 30): [028, 105, 115, 095, 054, 006, 120, 161, 187, 164, 091, 141, 036, 184, 071, 155, 066, 182, 189, 002, 191, 068, 090, 145, 163, 121, 180]
St. David (30 / 30): [149, 101, 150, 172, 165, 020, 049, 094, 060, 116, 056, 005, 093, 188, 043, 108, 192, 092, 167, 114, 012, 063, 077, 162, 085, 195, 032, 099, 084, 127]
University (24 / 30): [109, 003, 057, 170, 176, 100, 122, 080, 038, 082, 102, 052, 062, 055, 047, 074, 050, 179, 125, 186, 148, 103, 098, 190]
One common criterion for success when solving games like this is whether all of the medics have been assigned.
The following code allows us to see which residents (if any) were not matched to a hospital.
matched_residents = []
for _, residents in solution.items():
for resident in residents:
matched_residents.append(resident.name)
unmatched_residents = set(resident_preferences.keys()).difference(
matched_residents
)
unmatched_residentsset()
We’ve done it!
Every resident has successfully been assigned to a hospital of their choice with stability and fairness.