Help Santa optimize his Christmas run with Google OR-Tools, Geopandas, and Folium
By: Pierre-Louis Bescond
Tags: Google Or Tools, Routing, Editors Pick, Optimization, Data Science

Nice isn’t it? But maybe you are wondering how Santa does optimize the local deliveries in each neighborhood? Let’s zoom in!

Micro-level (neighborhood) optimization

Photo by Tom Rumble on Unsplash

So the same problem applies when Santa arrives in your neighborhood. He has to find the shortest path for every chimney! You (or your kids) are curious about what it would look like?

Obviously, this is going to be harder to find a database that registers every house around yours so let’s use a workaround 😀

We only need to get the coordinates of each street’s start and end-points. I have done it with 10 dummy streets:

street_names = ["Toys Avenue", "The Elves Boulevard", "Sleigh Ride", "Rudolph Alley", "Candy Street", "Reindeer Avenue", "The Artic Impasse", "Snowman Street", "Aurora Alley","Gingerbread street","Mrs Santa Avenue"] street_coordinates = { 'Start_lat': ['50.674380', '50.676395', '50.676882', '50.67703', '50.676882', '50.67571', '50.67504', '50.67438', '50.67571', '50.67504'], 'Start_long': ['3.096106', '3.093955', '3.094978', '3.097701', '3.094978', '3.096349', '3.097100', '3.096121', '3.096349', '3.097100'], 'End_lat': ['50.676395', '50.676882', '50.67703', '50.67602', '50.674700', '50.67504', '50.67470', '50.67470', '50.67662', '50.676312'], 'End_long': ['3.093955', '3.094978', '3.097701', '3.099203', '3.097481', '3.097100', '3.097481', '3.097481', '3.098355', '3.099718'] } neighboordhood = pd.DataFrame(street_coordinates, index=street_names, columns=["Start_long","Start_lat","End_long","End_lat"]).astype(float) neighboordhood

Image by Author

# Let's initialize our neighborhood map with Folium

neighboordhood_map = folium.Map(location=[50.675700, 3.096281],

# Custom tiles details:

tiles="https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",

attr="Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community",

zoom_start=17,

width="80%",

height="80%") for i in range(0, len(neighboordhood)):

folium.Marker([neighboordhood.iloc[i]['Start_lat'], neighboordhood.iloc[i]['Start_long']]).add_to(neighboordhood_map)

folium.Marker([neighboordhood.iloc[i]['End_lat'], neighboordhood.iloc[i]['End_long']]).add_to(neighboordhood_map) # And display the neighborhood map

neighboordhood_map

Image by Author

If we use the solver with only the start and endpoints of every street, we will end up with a clumsy solution, jumping from every corner to another:

Image by Author

So we will create artificial locations (ex. 30) between each start and endpoint to simulate the positions of the individual houses, as described below:

Image by Author

# Let's calculate the difference (longitude and latitude)

# between every street start and endpoint: neighboordhood["Delta_long"]=neighboordhood["End_long"]-neighboordhood["Start_long"] neighboordhood["Delta_lat"]=neighboordhood["End_lat"]-neighboordhood["Start_lat"] neighboordhood

Image by Author

# Number of intermediary locations

nb_steps = 30 neighboordhood_augmented_coordinates = pd.DataFrame(np.zeros((nb_steps*len(neighboordhood.index),3)), columns=["Street Name","Long","Lat"]) counter = 0 for street_name in neighboordhood.index:

# We define the starting point of the street

Local_Start_long = neighboordhood.loc[street_name,"Start_long"]

Local_Start_lat = neighboordhood.loc[street_name,"Start_lat"]



# We calculate the longitude and latitude steps

# to get from the start to the end

Long_step = neighboordhood.loc[street_name,"Delta_long"] / (nb_steps-1)

Lat_step = neighboordhood.loc[street_name,"Delta_lat"] / (nb_steps-1)

# We add the corresponding locations to the dataframe

for step in range(nb_steps):

neighboordhood_augmented_coordinates.loc[counter, "Street Name"] = street_name+"_"+str(step)

neighboordhood_augmented_coordinates.loc[counter, "Long"] = Local_Start_long + step * (Long_step)

neighboordhood_augmented_coordinates.loc[counter, "Lat"] = Local_Start_lat + step * (Lat_step) counter +=1 #print(street_name,"-> processed!") neighboordhood_augmented_coordinates = neighboordhood_augmented_coordinates.set_index("Street Name") # We remove duplicates locations (start and end points from

# connected streets)

neighboordhood_augmented_coordinates = neighboordhood_augmented_coordinates.drop_duplicates() # We calculate the distance matrix and convert it to a NumPy array

# We multiply the results by a 100.000 ratio as solver will round

# the values to preserve accuarcy neighboordhood_mtx = pd.DataFrame(distance_matrix(neighboordhood_augmented_coordinates.values, neighboordhood_augmented_coordinates.values), index=neighboordhood_augmented_coordinates.index,

columns=neighboordhood_augmented_coordinates.index)*100000 neighboordhood_mtx = round(neighboordhood_mtx) display(neighboordhood_mtx) neighboordhood_mtx = neighboordhood_mtx.to_numpy()

Image by Author

We repeat the same steps of initializing and running the routing solver as what we did for the capitals' journey.

Image by Author

A few opening questions before we close this article:

Do you wonder how long it would take to calculate the optimum path for the 26,562 cities registered in the initial dataset?

As you can imagine, the complexity of this route optimization is combinatorial and Google Colab also has its limits (a 12Gb RAM limit!)

The closest I have been able to get from the answer was a journey of ~11.000 cities (the ones that have more than 25.000 inhabitants) and the whole calculation took 2h40min:

Route optimization with 11k countries! Image by Author

With such a journey ahead of him, could it be easier for Santa to sub-contract some of the deliveries to the elves with additional sleighs?

What if each sleigh has a limited weight or volume capacity and Santa has to go back to Pole North every now and then during the night?

How can we optimize the load of the sleigh before each departure?

All these scenarios can be included in the calculation thanks to the search parameters of OR-Tools, as described in the Routing section documentation.

That’s oh-oh-all folks! 🎅🎄⛄

A huge thank you to Corentin Vasseur who reviewed this article before its release to help me improve it 🙏.

The complete notebook is available on Google Drive here.