Tutorial on displaying SHAP force plots in Python HTML | by Liam I. | Towards Data Science

Here’s the source code for this tutorial so that you can follow along, and you can just run app.py to see the results.
SHAP plots can be very useful for model explainability (see here for a great talk on them).
As part of a recent project on displaying a logistic regression of League of Legends data using SHAP (you can see the project web app here and a screenshot below), I struggled to find a way to display SHAP plots on page load without having to save the image first. I was so used to just using SHAP plots in a Jupyter notebook or something like that, but I really wanted to display it on a web app.
Since this was a large pain point for me, I wanted to consolidate what I learned and write about it here. I’ll cover some of the pit-falls for me in trying outbase64, plt.savefig(), the issues with these approaches, and I’ll end with the solution using HTML iFrames.
To give a visual of the result of this, an example end result is displaying SHAP force plots in a Flask web app:
Base64 is a binary-to-text encoding method. Using this, we can define our force_plot and use plt.savefig() to store it in a variable buf which we can later decode to ASCII. It’s not very important in my opinion to understand the functionality, but the result is that your computer will save each individual image and then display it in HTML. This means your img tag’s src variable will have to locate each saved image locally.
"""Example implementation of XGBoost algorithm and base64 approach to save the SHAP force plot and later display in HTML.If you're doing this in Flask, the ML is in app.py and you pass the SHAP force plot as a *variable* which you can index in your HTML page."""#Train test split#Let's assume you have some X_, y_...Xt, Xv, yt, yv = train_test_split(X_,y_, test_size=0.2, random_state=10)dt = xgb.DMatrix(Xt, label=yt.values, enable_categorical=True)dv = xgb.DMatrix(Xv, label=yv.values)#Tuned hyperparameters via hyperopt Bayesian optimizationparams = {   "eta": 0.5,   "max_depth": 8,   "min_child_weight": 1,   "objective": "binary:logistic",   "verbosity": 0,   "base_score": np.mean(yt),   "eval_metric": "logloss",   "colsample_bytree": 0.7434869381604485,   'gamma': 1.1053886968419446,   'reg_alpha': 49.0,   'reg_lambda': 0.9997899615065826}#Run modelmodel = xgb.train(params, dt, 35, [(dt, "train"), (dv, "valid")], early_stopping_rounds=5, verbose_eval=35)#SHAP explainer values (NumPy array)explainer = shap.TreeExplainer(model)shap_values = explainer.shap_values(Xv)#The SHAP *force* plot itself (using this method, I think you can #also do Beeswarm plots but I have not tried it)shap_plot = shap.force_plot(explainer.expected_value,     shap_values[-1:], features=Xv.iloc[-1:],     feature_names=Xv.columns[0:20],    matplotlib=True, show=False, plot_cmap=['#77dd77', '#f99191'])#Encode into base64,#Answer inspiration: https://stackoverflow.com/questions/60621103/is-there-a-way-to-render-shap-or-lime-output-from-flask-to-react/60669449#60669449buf = BytesIO()plt.savefig(buf,            format = "png",            dpi = 150,            bbox_inches = 'tight')dataToTake = base64.b64encode(buf.getbuffer()).decode("ascii")return dataToTake#Example HTML integration<img class="xgboost_image" src="data:image/png;base64">
While handy for single-plot generation, this is not workable in a production context as it means you will save each plot.
Using plt.savefig() we shorten our effort (Base64 is not entirely necessary), but it’s the same result of having to save each force plot and indexing it as a file in our <img src="wherever_the_plot_is_locally"> tag.
#Just using the same XGBoost model from above def shap_plot(ind):     explainer = shap.TreeExplainer(model)    shap_values = explainer.shap_values(Xv)    p = shap.force_plot(explainer.expected_value, shap_values[ind], Xv.iloc[[ind]])    plt.savefig('temp.svg')    plt.close()    return pshap_plot(3)
Easier to understand, yes, but again not available in a production context: you are still saving each image.
With this approach, we take advantage of HTML iFrames, which can embed our SHAP force_plot into our main HTML.
#works in Dash#taken from here: https://stackoverflow.com/questions/54292226/putting-html-output-from-shap-into-the-dash-output-layout-callbackdef _force_plot_html(*args):    force_plot = shap.force_plot(*args, matplotlib=False)    shap_html = f"<head>{shap.getjs()}</head><body>{force_plot.html()}</body>"    return html.Iframe(srcDoc=shap_html,                       style={"width": "100%", "height": "200px",                        "border": 0})
This does not exactly translate to what I’m using though, which is Flask. I took the above idea and appropriated it to a Flask context:
Let’s assume you have a logistic regression model like this:
Xt, Xv, yt, yv = train_test_split(X_, y_, test_size=0.2, random_state=10)model = LogisticRegression(penalty='l2', solver='liblinear', max_iter=900, C=0.1).fit(Xt, yt)explainer = shap.Explainer(model, Xt, feature_names=Xt.columns)shap_values = explainer(Xv)
The basic idea is in app.py to create a _force_plot_html function that uses explainer, shap_values, andind input to return a shap_html srcdoc. We will pass that shap_html variable to our HTML using render_template, and in the HTML file itself we will display shap_html in an embedded iFrame.
This is an example where we loop through ind, creating various SHAP plots, and displaying them in our HTML as iFrames.
#in app.pyfrom flask import *import shapfrom shap.plots._force_matplotlib import draw_additive_plotfrom model import give_shap_plotapp = Flask(__name__)@app.route('/')def displayshap():    explainer, shap_values = give_shap_plot()    def _force_plot_html(explainer, shap_values, ind):        force_plot = shap.plots.force(shap_values[ind],                      matplotlib=False)        shap_html = f"<head>{shap.getjs()}</head><body>{force_plot.html()}</body>"        return shap_html    shap_plots = {}    for i in range(10): #how many plots you want        ind = i        shap_plots[i] = _force_plot_html(explainer, shap_values, ind)       return render_template('displayshap.html', shap_plots = shap_plots)if __name__ == '__main__':    app.run()#EXIT app.py###<!-- HTML --><!-- in displayshap.html -->{% for i in shap_plots: %}    <iframe srcdoc="{{shap_plots[i]}}" style={"width": "2000px"; "height": "200px"; border: none;} ></iframe>{% endfor %}
The shap.getjs() is really important because without it you will get in your HTML a “Visualization omitted, Javascript library not loaded!’ issue.
Some drawbacks of this solution is that it only works for force plots.
I was not able to get beeswarm/bar plots like this to work:
#TYPES OF PLOTS THAT WORK: force_plot = shap.plots.force(shap_values[ind], matplotlib=False)#TYPES OF PLOTS THAT DO NOT WORK:force_plot = shap.plots.beeswarm(shap_values)force_plot = shap.plots.bar(shap_values[ind])#I think no .html() method is written for these? I'm not quite sure
Thank you for reading! I hope this is helpful to anyone wanting to incorporate SHAP into their web application. You can find the source code for this tutorial here.
You can find me on my website, LinkedIn, or github. You can read more of my stuff here.