In this notebook, we're going to create a dashboard that recommends scotches based on their taste profiles.
In [ ]:
%matplotlib widget
In [ ]:
import pandas as pd
import seaborn as sns
import numpy as np
import matplotlib.pyplot as plt
import os
In [ ]:
import ipywidgets as widgets
from traitlets import Unicode, List, Instance, link, HasTraits
from IPython.display import display, clear_output, HTML, Javascript
In [ ]:
display(widgets.Button())
In [ ]:
features = [[2, 2, 2, 0, 0, 2, 1, 2, 2, 2, 2, 2],
[3, 3, 1, 0, 0, 4, 3, 2, 2, 3, 3, 2],
[1, 3, 2, 0, 0, 2, 0, 0, 2, 2, 3, 1],
[4, 1, 4, 4, 0, 0, 2, 0, 1, 2, 1, 0],
[2, 2, 2, 0, 0, 1, 1, 1, 2, 3, 1, 3],
[2, 3, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1],
[0, 2, 0, 0, 0, 1, 1, 0, 2, 2, 3, 1],
[2, 3, 1, 0, 0, 2, 1, 2, 2, 2, 2, 2],
[2, 2, 1, 0, 0, 1, 0, 0, 2, 2, 2, 1],
[2, 3, 2, 1, 0, 0, 2, 0, 2, 1, 2, 3],
[4, 3, 2, 0, 0, 2, 1, 3, 3, 0, 1, 2],
[3, 2, 1, 0, 0, 3, 2, 1, 0, 2, 2, 2],
[4, 2, 2, 0, 0, 2, 2, 0, 2, 2, 2, 2],
[2, 2, 1, 0, 0, 2, 2, 0, 0, 2, 3, 1],
[3, 2, 2, 0, 0, 3, 1, 1, 2, 3, 2, 2],
[2, 2, 2, 0, 0, 2, 2, 1, 2, 2, 2, 2],
[1, 2, 1, 0, 0, 0, 1, 1, 0, 2, 2, 1],
[2, 2, 2, 0, 0, 1, 2, 2, 2, 2, 2, 2],
[2, 2, 3, 1, 0, 2, 2, 1, 1, 1, 1, 3],
[1, 1, 2, 2, 0, 2, 2, 1, 2, 2, 2, 3],
[1, 2, 1, 1, 0, 1, 1, 1, 1, 2, 2, 1],
[3, 1, 4, 2, 1, 0, 2, 0, 2, 1, 1, 0],
[1, 3, 1, 0, 0, 1, 1, 0, 2, 2, 2, 1],
[3, 2, 3, 3, 1, 0, 2, 0, 1, 1, 2, 0],
[2, 2, 2, 0, 1, 2, 2, 1, 2, 2, 1, 2],
[2, 3, 2, 1, 0, 0, 1, 0, 2, 2, 2, 1],
[4, 2, 2, 0, 0, 1, 2, 2, 2, 2, 2, 2],
[3, 2, 2, 1, 0, 1, 2, 2, 1, 2, 3, 2],
[2, 2, 2, 0, 0, 2, 1, 0, 1, 2, 2, 1],
[2, 2, 1, 0, 0, 2, 1, 1, 1, 3, 2, 2],
[2, 3, 1, 1, 0, 0, 0, 0, 1, 2, 2, 1],
[2, 3, 1, 0, 0, 2, 1, 1, 4, 2, 2, 2],
[2, 3, 1, 1, 1, 1, 1, 2, 0, 2, 0, 3],
[2, 3, 1, 0, 0, 2, 1, 1, 1, 1, 2, 1],
[2, 1, 3, 0, 0, 0, 3, 1, 0, 2, 2, 3],
[1, 2, 0, 0, 0, 1, 0, 1, 2, 1, 2, 1],
[2, 3, 1, 0, 0, 1, 2, 1, 2, 1, 2, 2],
[1, 2, 1, 0, 0, 1, 2, 1, 2, 2, 2, 1],
[3, 2, 1, 0, 0, 1, 2, 1, 1, 2, 2, 2],
[2, 2, 2, 2, 0, 1, 0, 1, 2, 2, 1, 3],
[1, 3, 1, 0, 0, 0, 1, 1, 1, 2, 0, 1],
[1, 3, 1, 0, 0, 1, 1, 0, 1, 2, 2, 1],
[4, 2, 2, 0, 0, 2, 1, 4, 2, 2, 2, 2],
[3, 2, 1, 0, 0, 2, 1, 2, 1, 2, 3, 2],
[2, 4, 1, 0, 0, 1, 2, 3, 2, 3, 2, 2],
[1, 3, 1, 0, 0, 0, 0, 0, 0, 2, 2, 1],
[1, 2, 0, 0, 0, 1, 1, 1, 2, 2, 3, 1],
[1, 2, 1, 0, 0, 1, 2, 0, 0, 2, 2, 1],
[2, 3, 1, 0, 0, 2, 2, 2, 1, 2, 2, 2],
[1, 2, 1, 0, 0, 1, 2, 0, 1, 2, 2, 1],
[2, 2, 1, 1, 0, 1, 2, 0, 2, 1, 2, 1],
[2, 3, 1, 0, 0, 1, 1, 2, 1, 2, 2, 2],
[2, 3, 1, 0, 0, 2, 2, 2, 2, 2, 1, 2],
[2, 2, 3, 1, 0, 2, 1, 1, 1, 2, 1, 3],
[1, 3, 1, 1, 0, 2, 2, 0, 1, 2, 1, 1],
[2, 1, 2, 2, 0, 1, 1, 0, 2, 1, 1, 3],
[2, 3, 1, 0, 0, 2, 2, 1, 2, 1, 2, 2],
[4, 1, 4, 4, 1, 0, 1, 2, 1, 1, 1, 0],
[4, 2, 4, 4, 1, 0, 0, 1, 1, 1, 0, 0],
[2, 3, 1, 0, 0, 1, 1, 2, 0, 1, 3, 1],
[1, 1, 1, 1, 0, 1, 1, 0, 1, 2, 1, 1],
[3, 2, 1, 0, 0, 1, 1, 1, 3, 3, 2, 2],
[4, 3, 1, 0, 0, 2, 1, 4, 2, 2, 3, 2],
[2, 1, 1, 0, 0, 1, 1, 1, 2, 1, 2, 1],
[2, 4, 1, 0, 0, 1, 0, 0, 2, 1, 1, 1],
[3, 2, 2, 0, 0, 2, 3, 3, 2, 1, 2, 2],
[2, 2, 2, 2, 0, 0, 2, 0, 2, 2, 2, 3],
[1, 2, 2, 0, 1, 2, 2, 1, 2, 3, 1, 3],
[2, 1, 2, 2, 1, 0, 1, 1, 2, 2, 2, 3],
[2, 3, 2, 1, 1, 1, 2, 1, 0, 2, 3, 1],
[3, 2, 2, 0, 0, 2, 2, 2, 2, 2, 3, 2],
[2, 2, 1, 1, 0, 2, 1, 1, 2, 2, 2, 2],
[2, 4, 1, 0, 0, 2, 1, 0, 0, 2, 1, 1],
[2, 2, 1, 0, 0, 1, 0, 1, 2, 2, 2, 1],
[2, 2, 2, 2, 0, 2, 2, 1, 2, 1, 0, 3],
[2, 2, 1, 0, 0, 2, 2, 2, 3, 3, 3, 2],
[2, 3, 1, 0, 0, 0, 2, 0, 2, 1, 3, 1],
[4, 2, 3, 3, 0, 1, 3, 0, 1, 2, 2, 0],
[1, 2, 1, 0, 0, 2, 0, 1, 1, 2, 2, 1],
[1, 3, 2, 0, 0, 0, 2, 0, 2, 1, 2, 1],
[2, 2, 2, 1, 0, 0, 2, 0, 0, 0, 2, 3],
[1, 1, 1, 0, 0, 1, 0, 0, 1, 2, 2, 1],
[2, 3, 2, 0, 0, 2, 2, 1, 1, 2, 0, 3],
[0, 3, 1, 0, 0, 2, 2, 1, 1, 2, 1, 1],
[2, 2, 1, 0, 0, 1, 0, 1, 2, 1, 0, 3],
[2, 3, 0, 0, 1, 0, 2, 1, 1, 2, 2, 1]]
feature_names = ['Body', 'Sweetness', 'Smoky',
'Medicinal', 'Tobacco', 'Honey',
'Spicy', 'Winey', 'Nutty',
'Malty', 'Fruity', 'cluster']
brand_names = ['Aberfeldy',
'Aberlour',
'AnCnoc',
'Ardbeg',
'Ardmore',
'ArranIsleOf',
'Auchentoshan',
'Auchroisk',
'Aultmore',
'Balblair',
'Balmenach',
'Belvenie',
'BenNevis',
'Benriach',
'Benrinnes',
'Benromach',
'Bladnoch',
'BlairAthol',
'Bowmore',
'Bruichladdich',
'Bunnahabhain',
'Caol Ila',
'Cardhu',
'Clynelish',
'Craigallechie',
'Craigganmore',
'Dailuaine',
'Dalmore',
'Dalwhinnie',
'Deanston',
'Dufftown',
'Edradour',
'GlenDeveronMacduff',
'GlenElgin',
'GlenGarioch',
'GlenGrant',
'GlenKeith',
'GlenMoray',
'GlenOrd',
'GlenScotia',
'GlenSpey',
'Glenallachie',
'Glendronach',
'Glendullan',
'Glenfarclas',
'Glenfiddich',
'Glengoyne',
'Glenkinchie',
'Glenlivet',
'Glenlossie',
'Glenmorangie',
'Glenrothes',
'Glenturret',
'Highland Park',
'Inchgower',
'Isle of Jura',
'Knochando',
'Lagavulin',
'Laphroig',
'Linkwood',
'Loch Lomond',
'Longmorn',
'Macallan',
'Mannochmore',
'Miltonduff',
'Mortlach',
'Oban',
'OldFettercairn',
'OldPulteney',
'RoyalBrackla',
'RoyalLochnagar',
'Scapa',
'Speyburn',
'Speyside',
'Springbank',
'Strathisla',
'Strathmill',
'Talisker',
'Tamdhu',
'Tamnavulin',
'Teaninich',
'Tobermory',
'Tomatin',
'Tomintoul',
'Tormore',
'Tullibardine']
In [ ]:
features_df = pd.DataFrame(features, columns=feature_names, index=brand_names)
features_df = features_df.drop('cluster', axis=1)
In [ ]:
norm = (features_df ** 2).sum(axis=1).apply('sqrt')
normed_df = features_df.divide(norm, axis=0)
sim_df = normed_df.dot(normed_df.T)
In [ ]:
def radar(df, ax=None):
# calculate evenly-spaced axis angles
num_vars = len(df.columns)
theta = 2*np.pi * np.linspace(0, 1-1./num_vars, num_vars)
# rotate theta such that the first axis is at the top
theta += np.pi/2
if not ax:
fig = plt.figure(figsize=(4, 4))
ax = fig.add_subplot(1,1,1, projection='polar')
else:
ax.clear()
for d, color in zip(df.itertuples(), sns.color_palette()):
ax.plot(theta, d[1:], color=color, alpha=0.7)
ax.fill(theta, d[1:], facecolor=color, alpha=0.5)
ax.set_xticklabels(df.columns)
legend = ax.legend(df.index, loc=(0.9, .95))
return ax
In [ ]:
In [ ]:
class RadarWidget(HasTraits):
factors_keys = List(['Aberfeldy'])
def __init__(self, df, **kwargs):
self.df = df
super(RadarWidget, self).__init__(**kwargs)
self.ax = None
self.factors_keys_changed()
def factors_keys_changed(self):
new_value = self.factors_keys
if self.ax:
self.ax.clear()
self.ax = radar(self.df.loc[new_value], self.ax)
We now define a get_similar( ) function to return the data of the top n similar scotches to a given scotch.
In [ ]:
def get_similar(name, n, top=True):
a = sim_df[name].sort_values(ascending=False)
a.name = 'Similarity'
df = pd.DataFrame(a) #.join(features_df).iloc[start:end]
return df.head(n) if top else df.tail(n)
We also need a function on_pick_scotch that will display a table of the top 5 similar scotches that Radar View watches, based on a given selected Scotch.
In [ ]:
def on_pick_scotch(Scotch):
name = Scotch
# Get top 6 similar whiskeys, and remove this one
top_df = get_similar(name, 6).iloc[1:]
# Get bottom 5 similar whiskeys
df = top_df
# Make table index a set of links that the radar widget will watch
df.index = ['''<a class="scotch" href="#" data-factors_keys='["{}","{}"]'>{}</a>'''.format(name, i, i) for i in df.index]
tmpl = f'''<p>If you like {name} you might want to try these five brands. Click one to see how its taste profile compares.</p>'''
prompt_w.value = tmpl
table.value = df.to_html(escape=False)
radar_w.factors_keys = [name]
plot = radar_w.factors_keys_changed()
In [ ]:
prompt_w = widgets.HTML(value='Aberfeldy')
display(prompt_w)
In [ ]:
table = widgets.HTML(
value="Hello <b>World</b>"
)
display(table)
In [ ]:
radar_w = RadarWidget(df=features_df)
In [ ]:
picker_w = widgets.interact(on_pick_scotch, Scotch=list(sim_df.index))
In [ ]:
radar_w.factors_keys
Powered by data from https://www.mathstat.strath.ac.uk/outreach/nessie/nessie_whisky.html and inspired by analysis from http://blog.revolutionanalytics.com/2013/12/k-means-clustering-86-single-malt-scotch-whiskies.html. This dashboard originated as a Jupyter Notebook.
In [ ]:
In [ ]: