How to Automate Analytics UX with Panel
Tags: Anaconda, Python, Data Science, Bokeh, Data Visualization

How to Automate Analytics UX with Panel Hector Follow Jan 7 · 12 min read

Data science and analytics teams create predictive models that help solve business questions, but most of these models are not implemented in products or embedded in applications, so where do they live? What happens when end users require the outputs of models but there is no product a data science team alone can build to store the model outputs?

In an effort to allow models or analyses created by data science teams to be easily exposed to users through standalone visualizations or production quality web applications, the Anaconda team has created the PyViz platform. This platform contains many libraries that make both visualization and data analysis in Python concise, uniform, and most importantly, repeatable.

Some data science & analytics developers may be familiar with Bokeh and Datashader, but a new tool in the PyViz world, Panel, was introduced at AnacondaCon 2019.

In this article we’ll go into some depth on:

Panel functionality Panel use cases Serving an app using Panel server Production deployments

Intro

We’re the Analytics and Data Science team at Under Armour working on global consumer engagement. Many of our use cases revolve around understanding the needs of our customers through our first party data and effectively predicting user responsiveness to marketing tests via algorithm development. A common issue we run into is that we don’t have a simple way to expose our predictive or explanatory models to end users without involving software engineering teams and building in house applications to expose our models. With our goal of providing trustworthy predictions quickly using our algorithms, we needed a tool that would expose the outputs of our generalized models and how they change based on user input.

Panel Functionality

What separates Panel from other tools in the PyViz ecosystem is the ability to bring together multiple visualization tools and their outputs into one centralized visualization — plotting library agnostic — and allowing end users to define data or model parameters via interactive widgets. All of the above also comes with a lightweight methodology for deploying Panel applications to a web server that your end users can access. Python scripts as well as Jupyter notebooks can be used to develop and deploy interactive applications.

Use Cases

Some example use cases where Panel could be used:

Say you have trained a forecasting model based on your company’s sales data and have extracted the model weights. Panel would allow an end user to then input new data sales time series data from your company and forecast it out with a variable number of periods based on your model, and additionally could show uncertainty intervals. You have trained the world’s best classifier on the MNIST dataset and saved the model. A user can then bring their own picture of a number drawn by hand, upload it via your web application and your model would show them the prediction and its probabilities. You have geospatial data that can predict when there will be the most traffic around bus terminals. Your end user would be able to move an hour-of-day slider left to right and look at predicted traffic or demand around bus terminals to see when they should buy their tickets.

If you would like more potential use cases, we encourage you to check out the Panel gallery.

Serving an App

Now we will take you on an end to end walk through running a basic analysis and deploying that app to a Panel server on your local machine. For the purposes of this post we will be using tutorial data and creating an ARIMA model, with little to no predictive power, and deploying that model. The aim of this is to show how to deploy a model, not how to create a model that can effectively predict something.

Open up your terminal and make sure the conda environment you want to use is activated.

Install Panel, using the pyviz channel with the command below.

conda install -c pyviz panel

Then open up a notebook or Python editor and get data.

We’ll be using one of Seaborn’s datasets on flight passengers from the 1950's.

You will notice the self notation in this method—this is because we're building this out as part of a larger Panel class.



df = pd.read_csv('

df['month'] = df['month'].\

apply(lambda x: strptime(x,'%B').tm_mon)

df['date'] = df[['year', 'month']].\

apply(lambda x: '-'.join(x.astype(str)), axis=1)+'-01'

df['date'] = pd.to_datetime(df['date']).dt.date

df = df[['date','passengers']]

self.data_frame = df.copy() def get_data(self):df = pd.read_csv(' https://raw.githubusercontent.com/mwaskom/seaborn-data/master/flights.csv' df['month'] = df['month'].\apply(lambda x: strptime(x,'%B').tm_mon)df['date'] = df[['year', 'month']].\apply(lambda x: '-'.join(x.astype(str)), axis=1)+'-01'df['date'] = pd.to_datetime(df['date']).dt.datedf = df[['date','passengers']]self.data_frame = df.copy()

We’re setting the data frame as a class attribute for later use.

Add a date range slider for filtering model data

Now we’ll go into the Panel decorators which allow for callbacks depending on user interactions with widgets.

There are many different types of widgets that can be used through Panel, and if there’s not currently one supported you can make it yourself with javascript and have it callback to the Panel application as well! See the list of currently supported widgets.

For this use case we’ll add a date range slider to allow users to filter the data that is fed into the model.

It’s as easy as this:

date_slider = pn.widgets.DateRangeSlider(

name='Date Range Slider',

start=dt.datetime(1949, 1, 1),

end=dt.datetime(1960, 12, 1),

value=(dt.datetime(1949, 1, 1), dt.datetime(1960, 12, 1)),

width=300, bar_color='#D15555')

Now when you have functions that depend on the slider you only need to put the following decorator above your method definition.

@param.depends('date_slider.value')

Create an ARIMA model on the data

Now we can define the model and plot predicted vs actual values.

This is a basic ARIMA exponential smoothing model.



def arima_model(self):

df_model = self.data_frame.copy()

df_model = df_model.\

set_index(pd.DatetimeIndex(df_model['date'],freq='MS')).\

drop('date',axis=1)

df_model = df_model[df_model.index > self.date_slider.value[0]]

df_model = df_model[df_model.index < self.date_slider.value[1]]



# basic exponential smoothing model

model = ARIMA(df_model, order=(0,1,1),freq='MS')

model_fit = model.fit(disp=0)



fitted_df = df_model.copy().\

rename({'passengers':'actual'},axis=1)

fitted_df['fitted'] = model_fit.predict(typ='levels')

fitted_df['error'] = abs(fitted_df['actual'] - fitted_df['fitted'])

fitted_df['perc_error'] = fitted_df['error']/fitted_df['actual']

fitted_df.reset_index(inplace=True,drop=False)



p = figure(plot_width=800, plot_height=266)

p.line('date','actual',source=fitted_df,

legend='Actual', color='red')

glyph = p.line('date','fitted',source=fitted_df,

legend='Fitted', color='blue')

p.yaxis.formatter = NumeralTickFormatter(format="0,0")

p.xaxis.formatter = DatetimeTickFormatter(months = "%m/%Y")

p.title.text = 'Actual vs Fitted ARIMA Values'

p.xaxis.axis_label = 'Date'

p.yaxis.axis_label = 'Passengers'

p.xgrid.grid_line_color = None

p.ygrid.grid_line_color = None

hover = HoverTool(renderers=[glyph],

tooltips=[("Date","

("Fitted","

("Actual","

formatters={"date": "datetime"},

mode='vline'

)

p.tools = [SaveTool(), PanTool(), hover, CrosshairTool()]

return p @param .depends('date_slider.value')def arima_model(self):df_model = self.data_frame.copy()df_model = df_model.\set_index(pd.DatetimeIndex(df_model['date'],freq='MS')).\drop('date',axis=1)df_model = df_model[df_model.index > self.date_slider.value[0]]df_model = df_model[df_model.index < self.date_slider.value[1]]# basic exponential smoothing modelmodel = ARIMA(df_model, order=(0,1,1),freq='MS')model_fit = model.fit(disp=0)fitted_df = df_model.copy().\rename({'passengers':'actual'},axis=1)fitted_df['fitted'] = model_fit.predict(typ='levels')fitted_df['error'] = abs(fitted_df['actual'] - fitted_df['fitted'])fitted_df['perc_error'] = fitted_df['error']/fitted_df['actual']fitted_df.reset_index(inplace=True,drop=False)p = figure(plot_width=800, plot_height=266)p.line('date','actual',source=fitted_df,legend='Actual', color='red')glyph = p.line('date','fitted',source=fitted_df,legend='Fitted', color='blue')p.yaxis.formatter = NumeralTickFormatter(format="0,0")p.xaxis.formatter = DatetimeTickFormatter(months = "%m/%Y")p.title.text = 'Actual vs Fitted ARIMA Values'p.xaxis.axis_label = 'Date'p.yaxis.axis_label = 'Passengers'p.xgrid.grid_line_color = Nonep.ygrid.grid_line_color = Nonehover = HoverTool(renderers=[glyph],tooltips=[("Date"," @date {%F}"),("Fitted"," @fitted "),("Actual"," @actual ")],formatters={"date": "datetime"},mode='vline'p.tools = [SaveTool(), PanTool(), hover, CrosshairTool()]return p

Once the model is trained, a Bokeh plot is created which compares actual vs fitted values and adds tooltips to the chart.

Create the Panel layout of the application

Now we can choose how we want to layout our application.

Since we only have one chart it’s fairly simple, but you can also create columns and rows and insert multiple charts in the same format.

We also call the get_data() method here to make sure the data attribute is set.

The full file should look like the following block.

Notice at the bottom, we call the class object and the panel() method so it's ready to serve.



import param

import datetime as dt

import panel as pn

from statsmodels.tsa.arima_model import ARIMA



from bokeh.models import HoverTool, SaveTool, PanTool, CrosshairTool

from bokeh.models.formatters import NumeralTickFormatter,DatetimeTickFormatter

from bokeh.plotting import figure



from time import strptime



pn.extension()



class panel_class(param.Parameterized):



title = 'Forecasting Airline Passengers'

subtitle = pn.pane.Str('Actual vs ARIMA Model',

style={'font-size': '15pt'})



date_slider = pn.widgets.DateRangeSlider(

name='Date Range Slider',

start=dt.datetime(1949, 1, 1), end=dt.datetime(1960, 12, 1),

value=(dt.datetime(1949, 1, 1),

dt.datetime(1960, 12, 1)),width=300,

bar_color='#D15555')



def get_data(self):

df = pd.read_csv('

df['month'] = df['month'].\

apply(lambda x: strptime(x,'%B').tm_mon)

df['date'] = df[['year', 'month']].\

apply(lambda x: '-'.join(x.astype(str)), axis=1)+'-01'

df['date'] = pd.to_datetime(df['date']).dt.date

df = df[['date','passengers']]

self.data_frame = df.copy()





def arima_model(self):

df_model = self.data_frame.copy()

df_model = df_model.\

set_index(pd.DatetimeIndex(df_model['date'],freq='MS')).\

drop('date',axis=1)

df_model = df_model[df_model.index > self.date_slider.value[0]]

df_model = df_model[df_model.index < self.date_slider.value[1]] import pandas as pdimport paramimport datetime as dtimport panel as pnfrom statsmodels.tsa.arima_model import ARIMAfrom bokeh.models import HoverTool, SaveTool, PanTool, CrosshairToolfrom bokeh.models.formatters import NumeralTickFormatter,DatetimeTickFormatterfrom bokeh.plotting import figurefrom time import strptimepn.extension()class panel_class(param.Parameterized):title = 'Forecasting Airline Passengers'subtitle = pn.pane.Str('Actual vs ARIMA Model',style={'font-size': '15pt'})date_slider = pn.widgets.DateRangeSlider(name='Date Range Slider',start=dt.datetime(1949, 1, 1), end=dt.datetime(1960, 12, 1),value=(dt.datetime(1949, 1, 1),dt.datetime(1960, 12, 1)),width=300,bar_color='#D15555')def get_data(self):df = pd.read_csv(' https://raw.githubusercontent.com/mwaskom/seaborn-data/master/flights.csv' df['month'] = df['month'].\apply(lambda x: strptime(x,'%B').tm_mon)df['date'] = df[['year', 'month']].\apply(lambda x: '-'.join(x.astype(str)), axis=1)+'-01'df['date'] = pd.to_datetime(df['date']).dt.datedf = df[['date','passengers']]self.data_frame = df.copy() @param .depends('date_slider.value')def arima_model(self):df_model = self.data_frame.copy()df_model = df_model.\set_index(pd.DatetimeIndex(df_model['date'],freq='MS')).\drop('date',axis=1)df_model = df_model[df_model.index > self.date_slider.value[0]]df_model = df_model[df_model.index < self.date_slider.value[1]]

model = ARIMA(df_model, order=(0,1,1),freq='MS')

model_fit = model.fit(disp=0)



fitted_df = df_model.copy().\

rename({'passengers':'actual'},axis=1)

fitted_df['fitted'] = model_fit.predict(typ='levels')

fitted_df['error'] = abs(fitted_df['actual'] - fitted_df['fitted'])

fitted_df['perc_error'] = fitted_df['error']/fitted_df['actual']

fitted_df.reset_index(inplace=True,drop=False)



p = figure(plot_width=800, plot_height=266)

p.line('date','actual',

source=fitted_df, legend='Actual',

color='red')

glyph = p.line('date','fitted',

source=fitted_df,

legend='Fitted',

color='blue')

p.yaxis.formatter = NumeralTickFormatter(format="0,0")

p.xaxis.formatter = DatetimeTickFormatter(months = "%m/%Y")

p.title.text = 'Actual vs Fitted ARIMA Values'

p.xaxis.axis_label = 'Date'

p.yaxis.axis_label = 'Passengers'

p.xgrid.grid_line_color = None

p.ygrid.grid_line_color = None

hover = HoverTool(renderers=[glyph],

tooltips=[ ("Date","

("Fitted","

("Actual","

formatters={"date": "datetime"},

mode='vline'

)

p.tools = [SaveTool(), PanTool(), hover, CrosshairTool()]

return p





def header(self):

title_panel = pn.pane.Str(self.title,

style={'font-size': '20pt',

'font-family': "Times New Roman"})

return title_panel





def subheader(self):

return self.subtitle



def panel(self):

logo = """<a href="

<img src="

self.get_data()



return pn.Row(

pn.Column(logo,self.date_slider),

pn.Column(self.header,self.subheader,self.arima_model))



panel_class().panel().servable() # basic exponential smoothing modelmodel = ARIMA(df_model, order=(0,1,1),freq='MS')model_fit = model.fit(disp=0)fitted_df = df_model.copy().\rename({'passengers':'actual'},axis=1)fitted_df['fitted'] = model_fit.predict(typ='levels')fitted_df['error'] = abs(fitted_df['actual'] - fitted_df['fitted'])fitted_df['perc_error'] = fitted_df['error']/fitted_df['actual']fitted_df.reset_index(inplace=True,drop=False)p = figure(plot_width=800, plot_height=266)p.line('date','actual',source=fitted_df, legend='Actual',color='red')glyph = p.line('date','fitted',source=fitted_df,legend='Fitted',color='blue')p.yaxis.formatter = NumeralTickFormatter(format="0,0")p.xaxis.formatter = DatetimeTickFormatter(months = "%m/%Y")p.title.text = 'Actual vs Fitted ARIMA Values'p.xaxis.axis_label = 'Date'p.yaxis.axis_label = 'Passengers'p.xgrid.grid_line_color = Nonep.ygrid.grid_line_color = Nonehover = HoverTool(renderers=[glyph],tooltips=[ ("Date"," @date {%F}"),("Fitted"," @fitted "),("Actual"," @actual ")],formatters={"date": "datetime"},mode='vline'p.tools = [SaveTool(), PanTool(), hover, CrosshairTool()]return p @param .depends('title')def header(self):title_panel = pn.pane.Str(self.title,style={'font-size': '20pt','font-family': "Times New Roman"})return title_panel @param .depends('subtitle')def subheader(self):return self.subtitledef panel(self):logo = """http://panel.pyviz.org "> https://panel.pyviz.org/_static/logo_stacked.png " width=200 height=150 align="center" margin=20px>"""self.get_data()return pn.Row(pn.Column(logo,self.date_slider),pn.Column(self.header,self.subheader,self.arima_model))panel_class().panel().servable()

Once your app is ready, go ahead and deploy. On the command line, move to the directory that houses your script. Once you are there make sure you have the correct conda environment, with Panel installed and activated. Once there, run the following command:

panel serve tutorial.py

This will create a Bokeh server on your local machine on the default port 5006. If you go http://localhost:5006/tutorial you should see the visualization.

Production Deployment on AWS EC2

Now that we’ve gone through creating a local server to showcase our app, we can move onto deploying apps to production cloud environments.

For the purposes of this article we will be using AWS EC2 instances. While none of the Panel code relies directly on EC2, our description of setting up instances and opening ports is all based on EC2. If you use another cloud provider, please refer to their documentation for setting up the infrastructure.

First, create an EC2 instance with an Ubuntu OS. If you want to restrict your applications to users only behind your organization’s private VPC or other private network — especially if you are handling any personal user data at all — make sure to deploy your EC2 instance on a private subnet.

Next, install Anaconda on the EC2 instance. Once the Anaconda base environment is ready you can share environment files (either manually copying or pulling from a source control repository) to create your specific environments between your local machine and the EC2 instance to make sure deployment environments are as close as possible to experimentation and development environments.

Modifying your Script to Serve Content

Now we can explore one of the most useful utilities provided by the Bokeh server: the server class. The server class is exposed through the bokeh.server library and allows users to define a server object's applications and all other command line configurations through pure Python code, as well as start and stop a server from within the script itself. This means we can choose to expose multiple applications to the server via a uniform and automated manner, and deploy them all on the same server and port.

The following chunk will find all Python scripts within the same directory and sub-directories as the original file and return a list with the loaded Panel applications as objects, and another list of their respective file names for each project.

import os

import json

import sys



from importlib.util import spec_from_loader, module_from_spec

from importlib.machinery import SourceFileLoader



def get_modules(self,set_attribs=False):



path = os.path.dirname(os.path.abspath(__file__))



subdir = []

for i in os.scandir(path):

if i.is_dir():

subdir.append(i.path)



subdir = list(filter(lambda k: 'ipynb' not in k and 'spy' not in k, subdir))



filedir = []

for i in subdir:

for x in os.scandir(i):

if x.is_file() and x.path.endswith('.py')==True and x.path.endswith('main.py')==False:

filedir.append(x.path)



filedir_name = [x.rsplit('/',1)[1] for x in filedir] # everything after last slash

filedir_name = [x.rsplit('.py',1)[0] for x in filedir_name] # everything before .py

modules = []

for i in range(0,len(filedir)):

spec = spec_from_loader(name=filedir_name[i],

loader=SourceFileLoader(fullname=filedir_name[i],

path=filedir[i]))

mod = module_from_spec(spec) # evaluating module

spec.loader.exec_module(mod) # executing module

modules.append(mod) # adding module to list



objects = []

for i in range(0,len(modules)):

x = modules[i].\

panel_class(name=str.replace(filedir_name[i],'_',' ').title())

objects.append(x)



if set_attribs==True:

self.applications = objects

self.filedir_name = filedir_name

else:

return objects,filedir_name

Panel allows for either Python scripts, Jupyter notebooks, or full directories to be served as applications. A succinct way to deploy quickly is to use Python scripts, so in the code above we filter out .ipynb files and any other non .py files, but this could easily be switched to look for those files explicitly.

Once all your Panel objects are read into the list, we can proceed to creating the server object. Just before though, we have to specify a helper function which allows us to modify the inner functionality of the Panel objects we’re working with:

def modify_doc(self,panel,doc):

panel_obj = panel.panel()

title = panel.title

return panel_obj.server_doc(title=title,doc=doc)

Now we can create the server object.

In the following function we take the applications and application name list returned by the get_modules() function.

Then, depending on how many applications were found in the directory, we create Bokeh application objects using a function handler since Panel allows for callbacks in its applications.

Instead of writing the code itself here, we create a string object with the code which we later evaluate, in order to be able to automate the process of defining application objects in this function.

Once the loop has finished creating the string, we create a server object, and the first argument to the server object will be the evaluated string code holding our application objects in a format that the Bokeh server can deploy.

Afterwards, the port parameter is defined, a prefix is added if necessary, and websocket origins are defined.

The websocket origins parameter is important as it defines the IP addresses the server should listen on and accept requests from. Multiple origins may be specified here.

You should set your EC2 instance’s IP address here so the server can listen to requests on it.

If you want to allow multiple users or multiple sessions to be served at once, modify the num_procs parameter to allow multi threaded processes on the server.

The last line starts the server, and makes sure it runs until the server itself is shut down.

from bokeh.server.server import Server

from bokeh.application import Application

from bokeh.application.handlers.function import FunctionHandler

from functools import partial



def start_server(self,prefix_param='',port_param=5006):

objects = self.applications

filedir_name = self.filedir_name

base_command = '{'

for i in range(0,len(objects)):

if i < len(objects)-1:

command = "'/"+str(filedir_name[i])+"': Application(FunctionHandler(partial(self.modify_doc,objects["+str(i)+"]))),"

base_command = base_command+command

elif i==len(objects)-1:

command = "'/"+str(filedir_name[i])+"': Application(FunctionHandler(partial(self.modify_doc,objects["+str(i)+"])))}"

base_command = base_command+command



server = Server(applications=eval(base_command), # evaluate programmatically written code

port=port_param,

prefix='/'+prefix_param+'/',

allow_websocket_origin=['localhost:'+ str(port_param)], # add the ip address of your ec2 instance when deploying the application

num_procs=8)

server.run_until_shutdown()

Putting it all together for completeness, we have the programmatic_server class shown below.

At the bottom we have added code to be able to run this file from the command line.

from bokeh.server.server import Server

from bokeh.application import Application

from bokeh.application.handlers.function import FunctionHandler

from functools import partial

from importlib.util import spec_from_loader, module_from_spec

from importlib.machinery import SourceFileLoader



import os

import sys



class programmatic_server():



def get_modules(self,set_attribs=False):



path = os.path.dirname(os.path.abspath(__file__))



subdir = []

for i in os.scandir(path):

if i.is_dir():

subdir.append(i.path)



subdir = list(filter(lambda k: 'ipynb' not in k and 'spy' not in k, subdir))



filedir = []

for i in subdir:

for x in os.scandir(i):

if x.is_file() and x.path.endswith('.py')==True and x.path.endswith('main.py')==False:

filedir.append(x.path)



filedir_name = [x.rsplit('/',1)[1] for x in filedir]

filedir_name = [x.rsplit('.py',1)[0] for x in filedir_name]

modules = []

for i in range(0,len(filedir)):

spec = spec_from_loader(name=filedir_name[i],

loader=SourceFileLoader(fullname=filedir_name[i],

path=filedir[i]))

mod = module_from_spec(spec) # evaluating module

spec.loader.exec_module(mod) # executing module

modules.append(mod) # adding module to list



objects = []

for i in range(0,len(modules)):

x = modules[i].panel_class(name=str.replace(filedir_name[i],'_',' ').title())

objects.append(x)



if set_attribs==True:

self.applications = objects

self.filedir_name = filedir_name

else:

return objects,filedir_name



def modify_doc(self,panel,doc):

panel_obj = panel.panel()

title=panel.title

return panel_obj.server_doc(title=title,doc=doc)



def start_server(self,prefix_param='',port_param=5006):

objects = self.applications

filedir_name = self.filedir_name

base_command = '{'

for i in range(0,len(objects)):

if i < len(objects)-1:

command = "'/"+str(filedir_name[i])+"': Application(FunctionHandler(partial(self.modify_doc,objects["+str(i)+"]))),"

base_command = base_command+command

elif i==len(objects)-1:

command = "'/"+str(filedir_name[i])+"': Application(FunctionHandler(partial(self.modify_doc,objects["+str(i)+"])))}"

base_command = base_command+command



server = Server(applications=eval(base_command),

port=port_param,

prefix='/'+prefix_param+'/',

allow_websocket_origin=['localhost:'+str(port_param)],

num_procs=8)

server.run_until_shutdown()



if __name__ == "__main__":

# command line arguments

prefix = sys.argv[1]

port = sys.argv[2]

port = int(port)

# calling modules

serv_obj = programmatic_server()

serv_obj.get_modules(set_attribs=True)

serv_obj.start_server(prefix_param=prefix,port_param=port)

Save this script as programmatic_server.py.

Theoretically, you can now put N number of applications in your directory and the Bokeh server can serve all of them.

You can start your server with the following command on the terminal, making sure you are using the correct conda environment:

python programmatic_server.py <prefix_of_choice> <port_number>

Make sure the EC2 instance port you have chosen is open. This is done by editing the security groups configured on your instance to accept incoming requests on the port through the TCP protocol.

Once that’s ready, navigate to http://<ip_address>:<port_number>/<prefix>/ and if you have multiple applications you should see a Bokeh HTML page listing all the applications currently being served.

If you only have one application this index will not be present, but if you navigate to http://<ip_address>:<port_number>/<prefix>/<script_name> then you should see your application.

The index can be disabled with the --disable-index option and can also be customized, but that's outside the scope of this article.

Structure of Apps for Deployment

The methodology of deploying shown above will only work if all of your apps are structured in the same format.

One clear way to name your application is to use the script name as the name of the application (which is shown as the tab name and the main title on the page itself). Every script also includes a panel_class which holds all the pertinent code to run the visualization.

Each panel_class can have as many helper functions as is needed, the only requirement is a method named panel which includes the logic for rendering the application itself. This method should execute all data ingestion and transformation functions as well as modeling methods from the class.

This structure allows us to automate the serving of the apps in a concise manner.

The structure we use is the same as was shown in the tutorial earlier, the only difference is we remove the last line panel_class().panel().servable() as the server takes care of this functionality.

All the code used in this post can be found here: https://github.com/hrzumaeta/panel_post_tutorial

Now we have an in house server which can be exposed to users and can be created and automated purely in Python.

That’s it! Hopefully we could illustrate the value of a tool like Panel to data science teams and how it can allow for structure to be applied to models that drive decision making.

If you’ve run into similar problems and have thoughts on this approach, we’d love to hear them!

Many thanks to the rest of the full team Phil Kim, Kate Sullivan, Tanvi Kode, Calvin Lau, Kameron Wells and Amol Sachdeva who have all contributed to our current Panel implementation and to this post.

If you enjoyed this tutorial and want to learn more about this work or are interested joining one of our data teams, please visit our Careers page and follow us on LinkedIn or Instagram.