Streamlit review and demo: best of the Python data app tools

Streamlit has quickly become the hot thing in data app frameworks. We put it to the test to see how well it stands up to the hype. Come for the review, stay for the code demo, including detailed examples of Altair plots.

python
code
data apps
reviews
Author

Brian Kent

Published

May 12, 2021

Warning

Data science products evolve quickly and this review is likely out of date. It was our best understanding of this product as of May 2021.

Note

To avoid bias, Crosstab Data Science does not accept compensation from vendors for product reviews.

Streamlit launched in 2018 to help data scientists build data apps, emphasizing fast development with a pure Python approach. Our hands-on testing showed that it’s the real deal; if you want to build a good front-end for your Python data science project as quickly as possible, Streamlit is the clear choice. If you want complete control of layout, aesthetics, user interactions, and compute architecture, Streamlit can feel frustratingly confined.

A demo Streamlit app, with a Bayesian analysis of a simulated staged rollout experiment. Please visit the live version. The article about the app’s purpose and meaning is here

Highlights

Surprise & Delight Friction & Frustration
Easy to learn and fast development speed. The API is straightforward and the documentation is fantastic. Can’t build complex interactive patterns. No way to define custom callback functions.
It just works, right out-of-the-box. Nice defaults for functionality and aesthetics. Limited control over layout.
A recent round of funding and growing user base suggests a stable, popular tool worth the time to learn. Still working out some of the small bugs. Some key features still in beta.

What is a data app framework?

Streamlit is a data app framework, sometimes called a data dashboarding tool. Data apps are a fantastic way for data scientists to present their results. They give the audience interactive control of visualizations, engaging the user more intuitively with the narrative. Data app frameworks are code-based, which allows for more complexity than BI dashboarding tools. In my demo, for example, I simulate data and use a Bayesian model to analyze an experiment. This kind of model-based visualization is much easier to do with code than with the SQL-plus-GUI combination used by most BI dashboarding tools.

Streamlit is hot

Streamlit the company was in the news recently, announcing a series B funding round of $35 million, bringing their total to $62 million since 2019. The framework seems to be growing quickly in popularity, accruing more GitHub stars (14,500) than any other data app framework except Plotly Dash, in less than two years.

Streamlit is the most popular data app framework and growing in popularity the fastest, at least judging by the number of GitHub stars. Take that with a grain of salt, of course. Source: star-history.t9t.io

As of this writing, there are over 300 Streamlit-tagged questions on StackOverflow and about 75 posts per week in Streamlit’s forum. Updates are released about twice a month, which creates a reassuring sense that any bugs will be hammered out quickly. Lastly, Streamlit’s app creation package is free and open-source on Github under a permissive license, so there’s little fear the core tool will vanish in the event the company is acquired.

All in all, it seems well worth the investment of time and effort to learn Streamlit, from either the business or professional data science perspective.

Streamlit is impressively easy to learn and use

The first thing I noticed when starting out with Streamlit is how crisp and clean the documentation is. Just enough detail to get a nice-looking app running, but no more. Even the config file is well-documented.

Streamlit’s value prop is crystal clear on the company’s website.

Streamlit’s API design is equally elegant. Define a simple hierarchy of layout containers (the reactive sidebar is included automatically), then add plots and control widgets with imperative-style commands. Elements appear in their layout container in code order unless otherwise specified.

Streamlit re-runs the whole Python script whenever any app state changes. This enables an impressive level of interactivity without the need for custom callback functions. Widget objects in the code just work; they take whatever value the user chooses in the live app.

As a regular Python user but Streamlit novice, it took me just a few hours to build my first finished prototype. Because app development was so fast, I was able to spend more time thinking about the data science methods and narrative.

There are a couple of small rough edges. Debugging is clunky; I found myself running the app script in an IPython terminal to debug, side-by-side with the rendered version in a browser. Another peccadillo: the displayed value of slider widgets seems to have a minimum step size of 0.01, even when the slider allows for smaller increments. These kinds of things are annoyances, not deal-breakers.

Streamlit’s functionality is good enough

Streamlit’s strong opinions about layout and script execution maximize development speed and get you to “good-enough” functionality very quickly but can leave power users frustrated.

Unlike Plotly Dash, Streamlit does not allow the developer to define custom reactive behavior. In my experience, this is not a super common requirement—it’s not part of the demo for this article—but it does come up. One specific use case I’ve seen is to use brushing, panning, zooming, or point selection on one plot to control the display of other plots in the app. This does not seem possible in Streamlit.

Streamlit’s decision to re-run the whole app script when any state changes means slow things like data loading and preprocessing are repeated many times, unnecessarily. The framework mitigates this problem in two ways. First, function outputs can be cached in a lookup table with a simple decorator. When this article was first published, we noted the details about caching were a bit murky, but the company has done a good job filling out the documentation.

The second way to reduce compute time is to use a form container, released just last month. Widgets inside a form container don’t trigger script execution until the user pushes the form_submit_button, so the user can update several control knobs at once with only a single re-run of the code.

Layout options are limited with Streamlit. The only obvious way to organize content is in columns and on the built-in sidebar. The sidebar is very handy—I have spent way too much time in other frameworks tweaking the layout of control widgets.

I used the Altair plot library for my demo and I’m happy with how the plots turned out. Plotly plots did not work as smoothly; the figures would not resize automatically to the width of their container, despite my best efforts.

Streamlit hosting is just getting started

For open-source projects, Streamlit (the company) will host the app for free…with limits. This feature, called Streamlit Sharing, is still invite-only, but I received access quickly when I requested it. Like the rest of Streamlit’s documentation, the instructions for deploying and app are clear and straightforward; the crux of it is granting Streamlit access to your GitHub account and indicating the correct repo.

Streamlit Sharing is rather limited. Developers are limited to 3 apps, even for open source projects. Apps run on a single CPU with less than 1GB of RAM and storage, so many modern machine learning use cases are not feasible. And of course, most industry data science projects are not open source, so Streamlit Sharing is a non-starter.

Streamlit recently announced a hosting capability for private apps as well, called Streamlit for Teams. It is currently in beta and also invite-only; I have not yet tried it.

Bells, whistles, and gotchas

  • The built-in screencast tool works seamlessly.
  • App scripts can be automatically synced to an S3 bucket.
  • You can run an app script from a URL (e.g. a Github gist), which is a decent fall-back option for sharing if you can’t deploy from a server.
  • Streamlit installs a lot of dependencies.
  • By default, Streamlit sends usage statistics back to the mothership. This can be turned off in the config.
  • By default, Streamlit watches the entire file system for changes in case a running app needs to be refreshed. The documentation suggests this can be changed, although I didn’t confirm it.

Building the demo

This demo app shows a Bayesian analysis of a simulated staged rollout experiment. Here, I describe the nuts and bolts of the app, but there is also a detailed walk-through of the methodology in a separate article.1

Streamlit apps are Python scripts, run with the Streamlit server. As of this writing, I’m using Python 3.8 and Streamlit 0.81.1. The other requirements and versions are listed in the repo’s requirements file.

Streamlit has strong opinions, presumably intended to maximize development speed. One is that layout is controlled imperatively, with a hierarchy of spatial containers. My app, for example, starts with the title and a brief description at the top level of the hierarchy, which is the streamlit module itself:

import streamlit as st

st.set_page_config(layout="wide")  # this needs to be the first Streamlit command

st.title("Worst-Case Analysis for Feature Rollouts")

st.markdown("*Check out the [article](https://www.crosstab.io/articles/staged-rollout-analysis) for a detailed walk-through!*")

The sidebar exists by default and is the perfect place for control widgets. Adding these widgets—an integer input and floating point sliders, in my case—follows the same imperative pattern. The variable assigned to the widget takes that widget’s value whenever the user changes it, and can then be used later in the script.

st.sidebar.title("Control Panel")

st.sidebar.subheader("Prior belief about the click rate")

prior_sessions = st.sidebar.number_input(
    "Number of prior sessions",
    min_value=1,
    max_value=None,
    value=100,
    step=1,
    help="The higher this number, the stronger your belief before seeing the data.",
)

prior_click_rate = st.sidebar.slider(
    "Prior click rate",
    min_value=0.01,
    max_value=0.5,
    value=0.1,
    step=0.005,
    help="What you think the click rate is before beginning the experiment.",
)

Creating columns in the main body for plots is a functionality still (explicitly) in beta, but it seems to work nicely. In my case, I want three columns of equal width.

left_col, middle_col, right_col = st.beta_columns(3)

Streamlit’s second strong opinion is that the whole Python script is re-run whenever any app state changes. This means we don’t have to write our own callback functions, but it also means the slow steps of data loading and preprocessing (or simulation, in this demo) are repeated each time. To avoid this, Streamlit can cache function outputs in a fast-lookup hash table.

import numpy as np
import pandas as pd
from scipy.stats import binom, poisson, beta

@st.cache
def generate_data(click_rate, avg_daily_sessions, num_days):
    """Simulate session and click counts for some number of days."""

    sessions = poisson.rvs(mu=avg_daily_sessions, size=num_days, random_state=rng)
    clicks = binom.rvs(n=sessions, p=click_rate, size=num_days, random_state=rng)

    data = pd.DataFrame(
        {
            "day": range(num_days),
            "sessions": sessions,
            "clicks": clicks,
            "misses": sessions - clicks,
            "click_rate": clicks / sessions,
        }
    )

    return data

rng = np.random.default_rng(2)
data = generate_data(click_rate=0.085, avg_daily_sessions=500, num_days=14)

I use the Altair package for the individual plots. They are a bit complex, with multiple layers and tooltips, so I’ve simplified the code here for brevity. Here is the code for the first plot.

import altair as alt

distro_grid = np.linspace(0, 0.25, 300)
prior = beta(10, 90)
prior_pdf = pd.DataFrame(
    {"click_rate": distro_grid, "prior_pdf": [prior.pdf(x) for x in distro_grid]}
)

fig = (
    alt.Chart(prior_pdf)
    .mark_line(size=4)
    .encode(
        x=alt.X("click_rate", title="Click rate"),
        y=alt.Y("prior_pdf", title="Probability density"),
        tooltip=[
            alt.Tooltip("click_rate", title="Click rate", format=".3f"),
            alt.Tooltip("prior_pdf", title="Probability density", format=".3f"),
        ],
    )
)

Unless otherwise specified, items appear in their layout container in the same order they’re written in the code. Now that I have the first figure created, I place it on the canvas as the first element of the left column:

left_col.subheader("Prior belief about the click rate")
left_col.altair_chart(fig, use_container_width=True)

Streamlit can detect if the script is being run with the Streamlit server, or from a REPL. This is very handy for saving static versions of the app plots without duplicating code, although it doesn’t seem to be advertised much.

if st.util.env_util.is_repl():
    fig.save("worst_case_prior.svg")

The rest of the code concerns the statistical model and the Altair plots, but—amazingly—that’s pretty much all there is to it for the app machinery. To launch it, we run the server from a bash prompt:

$ streamlit run app.py

  You can now view your Streamlit app in your browser.

  Local URL: http://localhost:8501

Wrapping up

The Crosstab Kite does not accept affiliate or referral fees, so I can say without ulterior motives that Streamlit is a delight to use. Because it’s so straightforward to learn and use, it’s certainly worth the time to at least try it out. Streamlit’s hosting capability is limited; it’s reasonable for open source projects, but not really an option for business use cases at this point. We look forward to trying out the commercial product when it’s generally available.

Footnotes

  1. The original version of this article described an app about US federal judicial nominations. That app is still live as well.↩︎