What you want to do is create all lists of size k given a list of size N, this is Combinatorics- a fun part of Mathematics, which is used in gambling! To make sure your function works, check that your len(possible_combinations) = n!/ (k! (N-k)!). Oh look, it uses those fun factorials! So if you have a list of 7 ingredients and you want to find all possible sublists of 4 ingredients, you get: (7654321)/ ((4321)(321)) = (765)/(321)= 75 = 35 unique sublists!
Generating Last Word Cocktail Variations
Overview
I really enjoy making mixed drinks. One I've gotten quite familiar with is something called The Last Word. It's a prohibition era cocktail attributed to the Detroit Athletic Club which contains Green Chartreuse, Gin, Maraschino Liqueur and Lime Juice in equal parts. It looks and tastes good, and I really enjoy them. The best part about The Last Word is that you can create countless variations. All you need to do is swap out one of the ingredients with something else. There's a lot of lists out there but they are a little bit limited for my liking. What if I could programmatically create my own list of variations using an algorithm?
If you're not convinced watch this great video by Greg from How to Drink where he explains it better.
Generating the "Ultimate" List
During this experiment I'm going to just include generic names such as Gin
instead of Gordons Gin
, I'm also going to pair this list down for demonstration purposes in the code examples. You'll be able to modify the example yourself if you'd like to create your own tailored list.
For this first attempt we're going to create a function that given an array of ingredients, it will return every 4 item variation it can find. We're going to need to utilize a series of loops and recursion to make this possible. I'll be writing this using Python as it handles this type of thing pretty well.
ingredients = [
'Green Chartreuse',
'Yellow Chartreuse',
'Dark Rum',
'Lemon Juice',
'Gin',
'Lime Juice',
'Benedictine'
]
possible_combinations = []
def f(ingredients, drink=[]):
if len(drink) == 4:
possible_combinations.append(drink)
return
else:
for i in range(len(ingredients)):
f(ingredients[i+1:], drink+[ingredients[i]])
f(ingredients)
print(possible_combinations)
My good friend, Ph.D, and mathematical wizard, Lara Langdon, has some insight into the mathematical formulas behind what's happening here.
Awesome! The reason we don't end with duplicates is due to how the function is utilizing recursion within the for
loop. On the initial loop it's going to get the first ingredient in the ingredients
array and then call the function again after offsetting the ingredients
array by its current index. The list of ingredients will get smaller each time as each loop will fire off another series of loops which perform the same offset operation. The example below represents a series of loops, you'll see how each loop has a base, and the last item is shifted until there's no more possible 4 item combinations left.
# 'A' is the base
[ A B C D E F ]
^ ^ ^ ^
1 2 3 4
[ A B C D E F ]
^ ^ ^ ^
1 2 3 4
[ A B C D E F ]
^ ^ ^ ^
1 2 3 4
# 'B' is the base
[ B C D E F ]
^ ^ ^ ^
1 2 3 4
[ B C D E F ]
^ ^ ^ ^
1 2 3 4
# 'C' is the base
[ C D E F ]
^ ^ ^ ^
1 2 3 4
Using a series of print
statements you can see how it builds up all variations using Green Chartreuse
as the base, which is the first item in the ingredients
array. The first three items remain the same all the way down, and only the last item is changed. Once it's done it will remove Green Chartreuse
and create the next drink using the next index as its base, in this example it will be Yellow Chartreuse
.
['Yellow Chartreuse', 'Dark Rum', 'Lemon Juice', 'Gin', 'Lime Juice', 'Benedictine']
['Dark Rum', 'Lemon Juice', 'Gin', 'Lime Juice', 'Benedictine']
['Lemon Juice', 'Gin', 'Lime Juice', 'Benedictine']
['Gin', 'Lime Juice', 'Benedictine']
# Drink created! Green Chartreuse, Yellow Chartreuse, Dark Rum, Lemon Juice
['Lime Juice', 'Benedictine']
# Drink created! Green Chartreuse, Yellow Chartreuse, Dark Rum, Gin
['Benedictine']
# Drink created! Green Chartreuse,Yellow Chartreuse, Dark Rum, Lime Juice
[]
# Drink created! Green Chartreuse, Yellow Chartreuse, Dark Rum, Benedictine
You probably don't want to drink those
While the previous algorithm works, it discards the fact that each ingredient in a Last Word plays a part in making the drink what it is. The first algorithm allows certain variations which are undesirable, I'd be surprised if anyone wants to willingly drink something that comprises of Vodka, Whiskey, Gin and Rum in equal parts.
Let's try and make it a little smarter. For this second experiment we'll update our function so it creates unique variations across multiple arrays. One array will contain liquors, another will have liqueurs, and another miscellaneous ingredients. When the function runs it will create all unique variations possible across these arrays, but it won't pull more than the specified amount from each, allowing us to keep some form of composition in the results. In mathematics this is called a Cartesian Product.
Thanks, Lara! You can find the updated function below.
# Ingredient lists are shortened for demonstration purposes.
liquors = [
'Dark Rum',
'Vodka',
'Gin',
'Whisky',
]
liquers = [
'Green Chartreuse',
'Yellow Chartreuse',
'Benedictine'
]
misc = [
'Lime Juice',
'Lemon Juice',
]
ingredient_categories = [liquors, liquers, liquers, misc]
possible_combinations = []
def f(categories, drink=[], start=0):
if len(drink) == len(categories):
possible_combinations.append(drink)
return
for ingredient in categories[start]:
f(categories, drink+[ingredient], start+1)
f(ingredient_categories)
print(possible_combinations)
Similar to the previous function the operations are performed in a similar way. The function is recursively called until each item has appeared in the results. The primary difference is that the pointing occurs over multiple arrays as opposed to a single one. When the drink
array has the same length as the ingredients
array the function is returned and the loop is exited.
The example below illustrates how the function moves across multiple arrays to formulate every possible variation.
[ A B ] [ A B ] [ A B ] [ A B ]
^ ^ ^ ^
1 2 3 4
[ A B ] [ A B ] [ A B ][ A B ]
^ ^ ^ ^
1 2 3 4
[ A B ] [ A B ] [ A B ] [ A B ]
^ ^ ^ ^
1 2 3 4
[ A B ] [ A B ] [ A B ] [ A B ]
^ ^ ^ ^
1 2 3 4
[ A B ] [ A B ] [ A B ] [ A B ]
^ ^ ^ ^
1 2 3 4
[ A B ] [ A B ] [ A B ] [ A B ]
^ ^ ^ ^
1 2 3 4
[ A B ] [ A B ] [ A B ] [ A B ]
^ ^ ^ ^
1 2 3 4
[ A B ] [ A B ] [ A B ] [ A B ]
^ ^ ^ ^
1 2 3 4
# 'B' is now the base, and so on...
[ A B ] [ A B ] [ A B ] [ A B ]
^ ^ ^ ^
1 2 3 4
As a result we have ourselves a somewhat adequate list of Last Word variations. Depending on the amount of possible ingredient choices you provide you will end up with a lot of results as this compounds quite fast. In my tests with only a handful of possible ingredients the array length was over 5,000
.
Last Word Generator
I've created a Last Word variant generator using an expanded ingredient list from the examples. I can't promise that all of these variations will taste good though. Each ingredient should be added in equal parts (typically an ounce each) over ice and shaken.
The glass color and name of the drink are randomly generated. If you'd like to see the list of all available outputs you can do so here. Drink resposibly!
The Final Word
Thank you for reading! Huge thank you to Lara Langdon for her insight and quotes for this post. Also a big thank you to Greg from How to Drink for giving me permission to use their video.