Real-World Problem: Analytics Scripts and AI Models Trapped in Jupyter Notebooks
About six months ago, our Data team received an urgent request from Operations. They needed an internal tool to track daily revenue and test a customer sentiment analysis model. Initially, everything ran smoothly in Jupyter Notebook. Data was pulled accurately, Pandas filtered it cleanly, and the Matplotlib charts looked great.
However, trouble started the moment we handed the notebook over to the Operations and Marketing teams. They weren’t used to opening a terminal and constantly crashed the kernel by accidentally spamming the Shift + Enter shortcut.
Whenever a manager wanted to change a date filter or test a new snippet of text, I had to sit right next to them and edit the code manually. No matter how powerful an analytics script is, without an intuitive UI, it ends up collecting dust on a developer’s machine.
Why Is Bringing Python Scripts to the Web So Hard?
When setting out to solve this, I realized the biggest bottleneck was the divide between data logic and the frontend:
- Too many technologies to learn: To bring a Python function to the browser, you typically have to build a REST API with FastAPI, then struggle with HTML, CSS, JavaScript, or React. For a data analyst or someone new to Python, this learning curve is a massive burden.
- Headaches with state management: When a user drags a slider or selects a dropdown, you have to write custom JavaScript event listeners, send fetch requests, and update the DOM. Keeping the UI in sync alone can burn days of work.
- Server resource strain: The AI model checkpoint file weighs 260MB. If every user request reloaded the model from disk and re-queried 500,000 rows of data, an 8GB RAM server would crash almost immediately from out-of-memory errors.
3 Solutions We Tested Before Choosing Streamlit
Before finding the optimal solution, our team experimented with three different approaches internally:
1. FastAPI with Vanilla HTML/Bootstrap
FastAPI handles the backend fast and reliably, and the auto-generated Swagger UI makes testing APIs effortless. However, the bottleneck was entirely on the frontend.
When leadership asked for four interactive filters and three hourly trend charts, I spent three full days just writing JavaScript fetch calls and configuring Chart.js. Feature delivery was far too slow to keep up with the project’s fast-changing needs.
2. Plotly Dash
Dash is pure Python and renders gorgeous analytical charts. However, its nested callback model quickly turned the codebase into spaghetti as the UI expanded. Debugging cyclic callback errors was time-consuming and frustrating for newcomers.
3. Streamlit: The Chosen Approach
Streamlit solved the problem completely: not a single line of HTML or JavaScript required. You write code sequentially like a standard script from top to bottom; wherever you declare a component, the button or chart appears right there.
The only caveat is its re-run execution model: Streamlit re-executes the entire script whenever a user interacts with a widget. At first, this caused sluggish response times. But once we implemented proper caching and session state, page load performance improved dramatically.
Step-by-Step Guide: Building the Dashboard and Integrating the AI Model
Here are the step-by-step implementation details, from local development to production deployment.
Step 1: Set Up the Environment
Create a dedicated virtual environment to prevent package conflicts between projects:
# Create and activate virtual environment
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install required packages
pip install streamlit==1.32.0 pandas plotly transformers torch
Step 2: Build the Layout and Dashboard Controls
Create an app.py file with the business data visualization logic:
import streamlit as st
import pandas as pd
import numpy as np
import plotly.express as px
# Page configuration
st.set_page_config(
page_title="Production Data Dashboard",
layout="wide"
)
st.title("Data Analytics & AI Experimentation Dashboard")
# Sidebar filters
st.sidebar.header("Data Filters")
selected_category = st.sidebar.selectbox(
"Select product category:",
["All", "Hardware", "Software", "Cloud Services"]
)
# Load and cache transaction data
@st.cache_data
def load_data():
np.random.seed(42)
return pd.DataFrame({
"Date": pd.date_range(start="2026-01-01", periods=100),
"Category": np.random.choice(["Hardware", "Software", "Cloud Services"], size=100),
"Revenue": np.random.randint(10, 200, size=100),
"Quantity": np.random.randint(1, 20, size=100)
})
df = load_data()
# Process data filtering
filtered_df = df if selected_category == "All" else df[df["Category"] == selected_category]
# Display summary metrics
col1, col2, col3 = st.columns(3)
col1.metric("Total Revenue ($k)", f"{filtered_df['Revenue'].sum():,}")
col2.metric("Total Orders", f"{filtered_df['Quantity'].sum():,}")
col3.metric("Average Order Value", f"{filtered_df['Revenue'].mean():.1f}")
# Interactive Plotly chart
fig = px.line(
filtered_df,
x="Date",
y="Revenue",
color="Category",
title="Revenue Trends Over Time"
)
st.plotly_chart(fig, use_container_width=True)
Step 3: Optimize Performance with Proper Caching
In practice, when the dataset reached 200,000 rows, selecting a dropdown froze the page for 4.2 seconds because Pandas had to recalculate everything from scratch. After applying @st.cache_data, response times dropped to 85ms.
Streamlit clearly distinguishes between two caching mechanisms:
@st.cache_data: Used for serializable data (DataFrames, NumPy arrays, REST API query results). Any time-consuming data computation should be wrapped with this decorator.@st.cache_resource: Used for heavy, non-serializable objects. Typical examples include database connection pools, TensorFlow/PyTorch sessions, or Transformer models.
Step 4: Integrate the Sentiment Analysis NLP Pipeline
The distilbert-base-uncased-finetuned-sst-2-english model is approximately 268MB. Without @st.cache_resource, the server would reload this 268MB file into RAM every time a user clicked the analyze button, quickly exhausting system resources.
The code snippet below ensures the model is loaded into memory only once:
from transformers import pipeline
st.subheader("AI Assistant: Customer Feedback Analysis")
@st.cache_resource
def get_ai_model():
return pipeline("sentiment-analysis", model="distilbert-base-uncased-finetuned-sst-2-english")
ai_analyzer = get_ai_model()
user_input = st.text_area(
"Enter customer feedback:",
"The dashboard is extremely responsive and saves our team hours of manual work!"
)
if st.button("Run AI Analysis", type="primary"):
if user_input.strip():
with st.spinner("AI is processing..."):
result = ai_analyzer(user_input)[0]
label = result['label']
score = result['score']
if label == "POSITIVE":
st.success(f"Result: Positive (Confidence: {score:.2%})")
else:
st.error(f"Result: Negative (Confidence: {score:.2%})")
else:
st.warning("Please enter review text before analyzing.")
Step 5: Launch and Test the Application
To run the application locally, execute the following command in your terminal:
streamlit run app.py
By default, the application listens on http://localhost:8501. Simply edit your code and press Ctrl + S; the browser will immediately hot-reload the updated UI without restarting the process.
3 Crucial Lessons Learned from Running Streamlit in Production
After six months running in production on an Ubuntu server (4 vCPUs, 8GB RAM) serving over 30 internal users daily, I’ve gathered three critical principles:
- Never use global variables: All user sessions in Streamlit share the same Python process memory. Always use
st.session_stateto store per-user state, preventing one user’s data from overwriting another’s. - Control RAM usage during AI inference: When multiple users submit text for analysis simultaneously, CPU and RAM spike sharply. You should restrict input sequence length (e.g., maximum 512 tokens) or quantize the model to ONNX or 8-bit to slash RAM usage by up to 50%.
- Manage processes with Docker or systemd: Never just run
streamlit runin a terminal session and leave it. Configure a systemd service or containerize the application with Docker. If the app crashes or the server reboots, the service will recover automatically without manual intervention.

