Got Scotch?

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())

Load Data Top


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

In [ ]:


In [ ]: