import urllib
import yaml
def read_yaml_to_dict(where, filename):
"""Read in the YAML data from the URL."""
= "/".join((where, filename))
url with urllib.request.urlopen(url) as response:
= yaml.safe_load(response.read())
dictionary
return dictionary
= "https://zenodo.org/record/3688091/files"
base_url
= read_yaml_to_dict(base_url, "residents.yml")
resident_preferences = read_yaml_to_dict(base_url, "hospitals.yml")
hospital_preferences = read_yaml_to_dict(base_url, "capacities.yml") hospital_capacities
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:
= len(resident_preferences)
num_residents = len(hospital_preferences)
num_hospitals = sum(hospital_capacities.values())
total_spaces
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
= HospitalResident.create_from_dictionaries(
game
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.
= game.solve(optimal="resident") solution
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)
= set(resident_preferences.keys()).difference(
unmatched_residents
matched_residents
) unmatched_residents
set()
We’ve done it!
Every resident has successfully been assigned to a hospital of their choice with stability and fairness.