Streamlit完整教程,从基础到进阶

目录

Streamlit 完整教程

什么是 Streamlit

Streamlit 是一个开源 Python 库,专门为机器学习和数据科学应用设计。它允许开发者使用纯 Python 代码创建美观的 Web 应用,无需了解 HTML、CSS 或 JavaScript。

核心特点

  • 简单易用: 只需要几行代码就能创建应用
  • 快速迭代: 代码更改后应用自动刷新
  • 交互性强: 内置各种交互组件
  • 数据可视化: 原生支持多种图表库
  • 机器学习友好: 深度集成 ML 工作流
  • 部署简单: 一键部署到云端

环境准备

bash 复制代码
# 使用 pip 安装
pip install streamlit

# 升级到最新版本
pip install --upgrade streamlit

快速开始

示例

python 复制代码
# app.py
import streamlit as st

# 设置页面标题
st.title("我的第一个 Streamlit 应用")
st.header("简单示例")
st.subheader("欢迎来到 Streamlit 世界!")

# 显示文本
st.write("这是一个简单的文本示例。")
st.write("你可以在这里显示任何内容。")

# 显示标题
st.title("标题示例")
st.header("二级标题")
st.subheader("三级标题")

# 显示 Markdown
st.markdown("""
### Markdown 支持的标题
- 列表项 1
- 列表项 2
- **粗体文本**
- *斜体文本*
""")

# 显示数据
import pandas as pd
df = pd.DataFrame({
    '列1': [1, 2, 3, 4],
    '列2': [10, 20, 30, 40]
})
st.dataframe(df)

# 显示图表
import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(0, 10, 100)
y = np.sin(x)
fig, ax = plt.subplots()
ax.plot(x, y)
st.pyplot(fig)

代码说明

  1. 第1行:导入 Streamlit 库
  2. 第3-5行:设置页面标题、副标题和三级标题
  3. 第7-8行:使用 st.write() 显示普通文本
  4. 第11-15行:使用 Markdown 语法显示格式化文本
  5. 第17-21行:创建并显示 Pandas DataFrame
  6. 第23-28行:创建并显示 Matplotlib 图表

运行结果

运行 streamlit run app.py 后,浏览器会自动打开显示:

  • 页面标题和各级标题
  • 格式化的文本内容
  • 一个 4x2 的表格
  • 一个正弦波形图

代码解析

  • st.title(): 显示页面主标题
  • st.header(): 显示二级标题
  • st.subheader(): 显示三级标题
  • st.write(): 显示任意类型的内容
  • st.markdown(): 支持 Markdown 格式的文本
  • st.dataframe(): 显示表格数据
  • st.pyplot(): 显示 Matplotlib 图表

核心概念

概念1:Rerun 机制

定义:Streamlit 的核心特性,当代码发生变化时,整个应用会自动重新运行。

为什么重要:这使得开发者可以立即看到代码更改的效果,无需手动刷新页面,极大提高了开发效率。

工作原理
修改代码
保存文件
Streamlit 检测到变化
重新执行脚本
更新界面
浏览器刷新

示例

python 复制代码
# 每次修改代码后,应用会重新运行
import time
import streamlit as st

st.write("当前时间:", time.time())
st.button("点击我")  # 点击这个按钮不会触发 rerun

概念2:会话状态 (Session State)

定义:在应用重新运行时保持数据的机制。

为什么重要:由于 Streamlit 的 rerun 机制,常规变量会在每次 rerun 时重置。Session State 允许我们保持用户输入、计算结果等数据。

工作原理

Session State 存储在字典中,键值对形式存储,在应用会话期间保持存在。

示例

python 复制代码
import streamlit as st

# 初始化会话状态
if 'counter' not in st.session_state:
    st.session_state.counter = 0

# 显示计数器
st.write("计数器:", st.session_state.counter)

# 增加计数器
if st.button("增加"):
    st.session_state.counter += 1

# 重置计数器
if st.button("重置"):
    st.session_state.counter = 0

概念3:Widget 交互

定义:用户界面交互组件的总称。

为什么重要:Widget 让应用具有交互性,允许用户输入数据、选择选项、调整参数等。

工作原理

每个 Widget 都有唯一的键(key),其值会保存在 Session State 中。

示例

python 复制代码
import streamlit as st

# 文本输入
name = st.text_input("请输入您的名字", "默认值")
st.write("你好,", name)

# 选择框
option = st.selectbox("选择一个选项", ["选项1", "选项2", "选项3"])
st.write("你选择了:", option)

# 滑块
age = st.slider("选择年龄", 0, 100, 25)
st.write("你的年龄是:", age)

# 复选框
if st.checkbox("显示详细信息"):
    st.write("这里是一些详细信息。")

概念之间的关系

Rerun 机制
会话状态
Widget 交互
应用状态管理
用户体验优化

这些概念协同工作:Rerun 机制触发代码重新执行,会话状态保持数据,Widget 提供交互界面,三者共同实现了一个完整的交互式 Web 应用。

基础功能详解

功能1:文本和显示组件

用途:在应用中显示各种类型的内容。

基本语法

python 复制代码
st.title("主标题")
st.header("二级标题")
st.subheader("三级标题")
st.text("普通文本")
st.markdown("Markdown文本")
st.latex("数学公式")

参数说明

  • text: 要显示的文本内容
  • unsafe_allow_html: 是否允许 HTML 标签(默认 False)

简单示例

python 复制代码
import streamlit as st

st.title("文本显示示例")

# 基本文本显示
st.text("这是一个普通的文本")
st.write("这是 write 方法")  # write 更灵活,可以显示任何对象

# Markdown
st.markdown("""
## Markdown 示例

- 列表项 1
- 列表项 2
- **粗体**
- *斜体*
- [链接](https://streamlit.io)
""")

# LaTeX
st.latex(r"""
\int_{a}^{b} f(x) dx = F(b) - F(a)
""")

常见用法

python 复制代码
import pandas as pd
import numpy as np

# 显示数据
st.write("## 数据显示")

# DataFrame
data = pd.DataFrame({
    '姓名': ['张三', '李四', '王五'],
    '年龄': [25, 30, 35],
    '城市': ['北京', '上海', '广州']
})
st.dataframe(data)

# 表格格式化
st.write("### 格式化表格")
st.table(data.style.background_gradient(cmap='YlOrRd'))

# 显示图像
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
ax.bar(data['姓名'], data['年龄'])
st.pyplot(fig)
  • 使用 Markdown 格式化文本,更易读

  • 使用 st.write() 显示复杂对象,它会自动选择最佳显示方式

  • 对于大量数据,考虑使用 st.data_editor() 提供更好的交互体验

  • 不要过度使用 HTML,除非必要

  • LaTeX 公式需要使用原始字符串(加 r 前缀)


功能2:输入组件

用途:收集用户输入的数据。

基本语法

python 复制代码
# 文本输入
text = st.text_input("标签", 默认值, max_chars=100)
text_area = st.text_area("文本区域", 高度=200)

# 选择输入
select = st.selectbox("选择框", 选项列表)
multiselect = st.multiselect("多选框", 选项列表)
slider = st.slider("滑块", 最小值, 最大值, 默认值)

# 按钮和开关
button = st.button("按钮")
checkbox = st.checkbox("复选框")
switch = st.toggle("开关")

参数说明

  • label: 组件标签
  • value: 默认值
  • key: 组件的唯一标识
  • disabled: 是否禁用
  • help: 帮助文本

简单示例

python 复制代码
import streamlit as st

st.title("输入组件示例")

# 文本输入
name = st.text_input("请输入姓名")
st.write("你的名字是:", name)

# 滑块
age = st.slider("年龄", 0, 100, 25)
st.write("你选择了年龄:", age)

# 选择框
city = st.selectbox("选择城市", ["北京", "上海", "广州", "深圳"])
st.write("你选择了:", city)

# 复选框
if st.checkbox("显示更多选项"):
    hobby = st.text_input("输入爱好")
    st.write("你的爱好是:", hobby)

常见用法

python 复制代码
import streamlit as st
import pandas as pd

st.title("表单示例")

with st.form("my_form"):
    col1, col2 = st.columns(2)

    with col1:
        name = st.text_input("姓名")
        email = st.text_input("邮箱")

    with col2:
        age = st.number_input("年龄", 0, 120)
        gender = st.selectbox("性别", ["男", "女", "其他"])

    submitted = st.form_submit_button("提交")

    if submitted:
        data = {
            '姓名': name,
            '邮箱': email,
            '年龄': age,
            '性别': gender
        }
        st.write("提交的数据:")
        st.json(data)
  • 使用 st.form() 创建表单,避免频繁触发 rerun
  • 为每个组件设置 key 参数,特别是在循环中创建组件时
  • 使用 columns 组织界面布局

常见陷阱

  • 没有设置 key 的组件可能导致状态冲突
  • 文本输入框不要忘记设置最大字符数

功能3:布局组件

用途:组织和美化应用界面。

基本语法

python 复制代码
# 列布局
col1, col2 = st.columns(2)
col1.write("左侧内容")
col2.write("右侧内容")

# 侧边栏
st.sidebar.write("侧边栏内容")

# 标签页
tab1, tab2, tab3 = st.tabs(["标签1", "标签2", "标签3"])

# 容器
with st.container():
    st.write("容器内的内容")

参数说明

  • columns: 列的数量和宽度比例
  • gap: 列之间的间距
  • label: 标签或标题

简单示例

python 复制代码
import streamlit as st
import numpy as np
import matplotlib.pyplot as plt

st.title("布局示例")

# 侧边栏
st.sidebar.header("控制面板")
data_points = st.sidebar.slider("数据点数", 10, 100, 50)

# 主内容区域
st.header("主内容")

# 列布局
col1, col2, col3 = st.columns([1, 2, 1])

with col1:
    st.write("左侧面板")
    st.metric("指标", 42, "+5%")

with col2:
    # 主要图表
    x = np.linspace(0, 10, data_points)
    y = np.sin(x)
    fig, ax = plt.subplots()
    ax.plot(x, y)
    st.pyplot(fig)

with col3:
    st.write("右侧面板")
    st.selectbox("选择数据类型", ["正弦", "余弦", "正切"])

# 标签页
tab1, tab2 = st.tabs(["视图1", "视图2"])

with tab1:
    st.write("第一个标签页")
    st.dataframe(np.random.rand(5, 3))

with tab2:
    st.write("第二个标签页")
    st.line_chart(np.random.randn(20, 2))

常见用法

python 复制代码
import streamlit as st

st.title("高级布局")

# 使用 expanders 折叠内容
with st.expander("点击展开详细信息"):
    st.write("这里是展开的内容...")
    st.write("可以包含任何组件")

# 使用 divider 分隔内容
st.divider()

st.write("分隔符上方的部分")

# 使用空格布局
st.write("布局示例:")
st.write("-" * 50)

col1, col2 = st.columns([3, 1])
with col1:
    st.write("主内容占据更多空间")

with col2:
    st.write("侧边栏")

# 使用容器进行分组
with st.container():
    st.header("容器1")
    st.write("容器1的内容")

with st.container():
    st.header("容器2")
    st.write("容器2的内容")
  • 使用侧边栏放置控制选项,保持主界面整洁
  • 使用列布局创建多栏设计
  • 使用标签页组织相关内容

常见陷阱

  • 不要过度嵌套容器,影响性能
  • 确保布局在不同屏幕尺寸下都能正常显示

功能4:数据可视化

用途:以图表形式展示数据。

基本语法

python 复制代码
# 内置图表
st.line_chart(data)
st.bar_chart(data)
st.area_chart(data)
st.scatter_chart(data)

# Map 地图
st.map(data)

# 使用 Plotly
import plotly.express as px
fig = px.line(data, x='x', y='y')
st.plotly_chart(fig)

参数说明

  • data: 要显示的数据
  • use_container_width: 是否使用容器宽度
  • width: 图表宽度
  • height: 图表高度

简单示例

python 复制代码
import streamlit as st
import pandas as pd
import numpy as np

st.title("基本图表示例")

# 生成数据
chart_data = pd.DataFrame(
    np.random.randn(20, 3),
    columns=['A', 'B', 'C']
)

# 线图
st.subheader("线图")
st.line_chart(chart_data)

# 柱状图
st.subheader("柱状图")
st.bar_chart(chart_data.iloc[:5])  # 只显示前5行

# 面积图
st.subheader("面积图")
st.area_chart(chart_data)

# 散点图
st.subheader("散点图")
st.scatter_chart(
    chart_data,
    x='A',
    y='B',
    color='C'
)

常见用法

python 复制代码
import streamlit as st
import pandas as pd
import plotly.express as px

st.title("高级图表示例")

# 模拟销售数据
dates = pd.date_range('2024-01-01', periods=12, freq='M')
sales_data = pd.DataFrame({
    '日期': dates,
    '产品A': np.random.randint(1000, 5000, 12),
    '产品B': np.random.randint(800, 4000, 12),
    '产品C': np.random.randint(500, 3000, 12)
})

# 使用 Plotly 创建交互式图表
st.subheader("销售趋势 - Plotly")
fig = px.line(
    sales_data,
    x='日期',
    y=['产品A', '产品B', '产品C'],
    title='月度销售趋势',
    markers=True
)
fig.update_layout(
    xaxis_title='月份',
    yaxis_title='销售额',
    hovermode='x unified'
)
st.plotly_chart(fig, use_container_width=True)

# 地图可视化
st.subheader("地理位置分布")

# 模拟城市数据
cities = pd.DataFrame({
    'city': ['北京', '上海', '广州', '深圳'],
    'lat': [39.9042, 31.2304, 23.1291, 22.5431],
    'lon': [116.4074, 121.4737, 113.2644, 114.0579],
    'value': [2154, 2424, 1404, 1344]
})

st.map(
    cities,
    latitude='lat',
    longitude='lon',
    size='value',
    zoom=4
)
  • 对于快速原型,使用内置的简单图表
  • 对于需要交互性的图表,使用 Plotly
  • 考虑数据量,大数据集使用聚合显示

功能5:媒体组件

用途:在应用中显示图片、音频、视频等媒体内容。

基本语法

python 复制代码
# 图片
st.image(image, caption="图片说明", width=200)

# 音频
st.audio(audio_file, format="audio/mp3")

# 视频
st.video(video_file, format="video/mp4", start_time=0)

# 播放器
st.audio(audio_url, format="audio/mpeg", start_time=0)

参数说明

  • image: 图片路径或 URL
  • caption: 图片说明
  • width: 图片宽度
  • use_column_width: 是否使用列宽
  • format: 媒体格式
  • start_time: 开始时间(秒)

简单示例

python 复制代码
import streamlit as st

st.title("媒体组件示例")

# 显示图片
st.subheader("图片示例")
st.image(
    "https://streamlit.io/images/brand/streamlit-logo-secondary-colormark-darktext.png",
    caption="Streamlit Logo",
    width=300
)

# 音频播放器
st.subheader("音频播放器")
st.audio(
    "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3",
    format="audio/mp3"
)

# 视频播放器
st.subheader("视频播放器")
st.video(
    "https://www.learningcontainer.com/wp-content/uploads/2020/05/sample-mp4-file.mp4",
    format="video/mp4"
)

常见用法

python 复制代码
import streamlit as st
import numpy as np
from PIL import Image

st.title("高级媒体功能")

# 生成并显示图表图片
fig, ax = plt.subplots()
ax.bar(['A', 'B', 'C'], [1, 2, 3])
ax.set_title("动态生成的图表")

# 将图表转换为图片
import io
from matplotlib.backends.backend_agg import FigureCanvasAgg
buf = io.BytesIO()
fig.savefig(buf, format='png')
img = Image.open(buf)

st.image(img, caption="动态生成的图表", use_column_width=True)

# 文件上传
st.subheader("文件上传")
uploaded_file = st.file_uploader(
    "选择一个图片",
    type=['png', 'jpg', 'jpeg']
)

if uploaded_file is not None:
    image = Image.open(uploaded_file)
    st.image(image, caption="上传的图片")

    # 显示图片信息
    st.write(f"图片尺寸: {image.size}")
    st.write(f"图片模式: {image.mode}")
  • 使用 st.file_uploader() 让用户上传本地文件
  • 为媒体组件添加说明文字,提高可用性
  • 考虑文件大小,避免加载过大的媒体文件

功能6:缓存机制

用途:提高应用性能,避免重复计算。

基本语法

python 复制代码
# 简单缓存
@st.cache_data
def load_data():
    return expensive_computation()

# 带参数的缓存
@st.cache_data(ttl=3600)
def process_data(params):
    return compute(params)

# T 缓存对象
@st.cache_resource
def load_model():
    return load_ml_model()

参数说明

  • ttl: 缓存有效期(秒)
  • suppress_st_warning: 是否隐藏 Streamlit 警告
  • show_spinner: 是否显示加载动画
  • max_entries: 最大缓存条目数
  • hash_funcs: 自定义哈希函数

简单示例

python 复制代码
import streamlit as st
import time
import pandas as pd
import numpy as np

st.title("缓存示例")

# 模拟耗时的数据加载
@st.cache_data
def load_large_dataset():
    st.write("正在加载数据集...")
    time.sleep(3)  # 模拟耗时操作
    return pd.DataFrame(np.random.randn(1000, 10))

# 使用缓存
data = load_large_dataset()
st.write("数据加载完成!")
st.write(f"数据形状: {data.shape}")

# 清除缓存
if st.button("清除缓存"):
    st.cache_data.clear()
    st.rerun()  # 重新运行以重新加载数据

常见用法

python 复制代码
import streamlit as st
import time
from sklearn.datasets import make_classification
from sklearn.ensemble import RandomForestClassifier

st.title("机器学习模型缓存")

# 缓存数据集
@st.cache_data
def get_dataset(n_samples=1000):
    st.write("正在生成数据集...")
    X, y = make_classification(
        n_samples=n_samples,
        n_features=20,
        n_informative=10,
        n_redundant=5,
        random_state=42
    )
    return X, y

# 缓存模型
@st.cache_resource
def train_model(X, y, n_estimators=100):
    st.write("正在训练模型...")
    model = RandomForestClassifier(
        n_estimators=n_estimators,
        random_state=42
    )
    model.fit(X, y)
    return model

# 生成数据
X, y = get_dataset()

# 训练模型
model = train_model(X, y)

# 显示模型性能
accuracy = model.score(X, y)
st.metric("模型准确率", f"{accuracy:.2%}")
  • 对数据加载和计算密集的操作使用缓存
  • 为缓存函数添加描述性注释
  • 合理设置 TTL 值,平衡性能和数据新鲜度

功能7:错误处理和调试

用途:处理运行时错误,提高应用稳定性。

基本语法

python 复制代码
# 错误捕获
try:
    result = risky_operation()
except Exception as e:
    st.error(f"发生错误: {e}")

# 显示异常信息
st.exception(e)

# 使用 st.form 避免部分操作触发 rerun
with st.form("safe_operation"):
    # 代码在这里执行
    submitted = st.form_submit_button("提交")
    if submitted:
        # 执行可能出错的操作
        pass

# 调试信息
st.write("调试信息:", debug_data)

参数说明

  • exception: 要显示的异常对象
  • element: 要显示错误的组件
  • help: 帮助文本

简单示例

python 复制代码
import streamlit as st
import pandas as pd

st.title("错误处理示例")

# 输入文件名
filename = st.text_input("输入文件名 (例如: data.csv)")

# 加载文件
try:
    if filename:
        df = pd.read_csv(filename)
        st.write("文件加载成功!")
        st.write(f"数据形状: {df.shape}")
        st.dataframe(df.head())
    else:
        st.write("请输入文件名")

except FileNotFoundError:
    st.error("文件不存在,请检查文件名是否正确")

except pd.errors.EmptyDataError:
    st.error("文件为空,请检查文件内容")

except Exception as e:
    st.error(f"发生未知错误: {e}")

    # 显示详细错误信息
    with st.expander("查看错误详情"):
        st.exception(e)

常见用法

python 复制代码
import streamlit as st
import numpy as np
from sklearn.preprocessing import StandardScaler

st.title("数据处理示例")

# 文件上传
uploaded_file = st.file_uploader("上传 CSV 文件", type=["csv"])

if uploaded_file is not None:
    try:
        # 读取文件
        df = pd.read_csv(uploaded_file)

        # 数据预览
        st.subheader("数据预览")
        st.dataframe(df.head())

        # 数据质量检查
        st.subheader("数据质量")

        # 检查缺失值
        missing_values = df.isnull().sum()
        if missing_values.sum() > 0:
            st.warning("数据包含缺失值")
            st.write(missing_values[missing_values > 0])
        else:
            st.success("数据没有缺失值")

        # 检查异常值
        numerical_cols = df.select_dtypes(include=[np.number]).columns
        if len(numerical_cols) > 0:
            st.subheader("异常值检测")

            for col in numerical_cols:
                Q1 = df[col].quantile(0.25)
                Q3 = df[col].quantile(0.75)
                IQR = Q3 - Q1
                lower_bound = Q1 - 1.5 * IQR
                upper_bound = Q3 + 1.5 * IQR

                outliers = df[(df[col] < lower_bound) | (df[col] > upper_bound)]
                if len(outliers) > 0:
                    st.warning(f"{col} 列存在 {len(outliers)} 个异常值")
                else:
                    st.success(f"{col} 列没有异常值")

    except Exception as e:
        st.error(f"处理文件时发生错误: {e}")
        st.exception(e)
  • 使用 try-except 块处理可能出错的操作
  • 为用户提供清晰的错误信息
  • 使用 st.form() 确保操作的原子性

实践项目

项目目标

通过构建一个销售数据分析仪表板,综合运用前面学到的 Streamlit 知识,包括数据加载、可视化、交互控件和布局设计。

项目需求

  • 从 CSV 文件加载销售数据
  • 提供数据筛选功能(时间范围、产品类别)
  • 展示关键业务指标(销售额、利润等)
  • 创建交互式图表
  • 响应式布局设计
  • 数据导出功能

项目结构

复制代码
sales-dashboard/
├── app.py                 # 主应用文件
├── data/                 # 数据文件夹
│   └── sales_data.csv    # 示例销售数据
├── requirements.txt      # 依赖包
└── README.md            # 项目说明

实现步骤

步骤1:设置项目环境

目标:创建项目结构和基本依赖

代码

python 复制代码
# requirements.txt
streamlit>=1.0.0
pandas>=1.0.0
numpy>=1.0.0
plotly>=5.0.0
matplotlib>=3.0.0
seaborn>=0.11.0

解释

  • 定义项目所需的 Python 包
  • 使用版本号确保兼容性
  • 包括数据分析和可视化库

步骤2:生成示例数据

目标:创建模拟的销售数据文件

代码

python 复制代码
# generate_data.py
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
import random

# 生成日期范围
start_date = datetime(2024, 1, 1)
end_date = datetime(2024, 12, 31)
dates = pd.date_range(start=start_date, end=end_date, freq='D')

# 生成产品数据
products = [
    '笔记本电脑', '智能手机', '平板电脑', '耳机',
    '键盘', '鼠标', '显示器', '打印机'
]
categories = ['电子产品', '配件']

# 生成销售数据
data = []
for i in range(10000):
    date = random.choice(dates)
    product = random.choice(products)
    category = '电子产品' if product in ['笔记本电脑', '智能手机', '平板电脑'] else '配件'
    quantity = random.randint(1, 10)
    unit_price = random.randint(100, 5000)
    cost_price = unit_price * random.uniform(0.6, 0.9)

    # 添加一些季节性因素
    if date.month in [11, 12]:  # 黑色星期五和圣诞节
        unit_price *= random.uniform(0.8, 1.2)

    data.append({
        '日期': date,
        '产品': product,
        '类别': category,
        '数量': quantity,
        '单价': unit_price,
        '成本价': cost_price,
        '销售额': quantity * unit_price,
        '成本': quantity * cost_price,
        '利润': quantity * (unit_price - cost_price)
    })

# 创建 DataFrame
df = pd.DataFrame(data)

# 保存到 CSV
df.to_csv('data/sales_data.csv', index=False, encoding='utf-8-sig')
print(f"生成了 {len(df)} 条销售记录")

解释

  • 生成一年的日期数据
  • 创建多样化的产品和类别
  • 添加价格和数量的随机变化
  • 包含季节性销售模式
  • 计算销售额、成本和利润

步骤3:创建主应用文件

目标:构建完整的销售数据分析仪表板

代码

python 复制代码
# app.py
import streamlit as st
import pandas as pd
import numpy as np
import plotly.express as px
from datetime import datetime
import matplotlib.pyplot as plt
import seaborn as sns

# 页面配置
st.set_page_config(
    page_title="销售数据分析仪表板",
    page_icon="📊",
    layout="wide",
    initial_sidebar_state="expanded"
)

# 缓存数据加载
@st.cache_data
def load_data():
    df = pd.read_csv('data/sales_data.csv')
    # 转换日期列
    df['日期'] = pd.to_datetime(df['日期'])
    # 添加月份和年份列
    df['月份'] = df['日期'].dt.month
    df['年份'] = df['日期'].dt.year
    df['季度'] = df['日期'].dt.quarter
    return df

# 缓存计算指标
@st.cache_data
def calculate_metrics(df):
    metrics = {
        '总销售额': df['销售额'].sum(),
        '总利润': df['利润'].sum(),
        '总订单数': len(df),
        '平均订单金额': df['销售额'].mean(),
        '利润率': (df['利润'].sum() / df['销售额'].sum()) * 100
    }
    return metrics

# 主标题
st.title("📊 销售数据分析仪表板")
st.markdown("---")

# 加载数据
df = load_data()
metrics = calculate_metrics(df)

# 侧边栏 - 筛选器
st.sidebar.header("🔍 数据筛选")

# 日期范围筛选
st.sidebar.subheader("日期范围")
min_date = df['日期'].min().to_pydatetime()
max_date = df['日期'].max().to_pydatetime()
selected_dates = st.sidebar.date_input(
    "选择日期范围",
    [min_date, max_date],
    min_value=min_date,
    max_value=max_date
)

if len(selected_dates) == 2:
    start_date, end_date = selected_dates
    filtered_df = df[(df['日期'] >= pd.Timestamp(start_date)) &
                     (df['日期'] <= pd.Timestamp(end_date))]
else:
    filtered_df = df

# 产品类别筛选
st.sidebar.subheader("产品类别")
categories = filtered_df['类别'].unique()
selected_categories = st.sidebar.multiselect(
    "选择类别",
    categories,
    default=categories
)

filtered_df = filtered_df[filtered_df['类别'].isin(selected_categories)]

# 产品筛选
st.sidebar.subheader("产品")
products = filtered_df['产品'].unique()
selected_products = st.sidebar.multiselect(
    "选择产品",
    products,
    default=products
)

if selected_products:
    filtered_df = filtered_df[filtered_df['产品'].isin(selected_products)]

# 显示筛选结果
st.sidebar.metric(
    "筛选后记录数",
    len(filtered_df)
)

# 主要指标卡片
col1, col2, col3, col4, col5 = st.columns(5)

with col1:
    st.metric("总销售额", f"¥{metrics['总销售额']:,.0f}",
              delta=None, delta_color="normal")

with col2:
    st.metric("总利润", f"¥{metrics['总利润']:,.0f}",
              delta=None, delta_color="normal")

with col3:
    st.metric("订单数", f"{metrics['总订单数']:,}",
              delta=None, delta_color="normal")

with col4:
    st.metric("平均订单金额", f"¥{metrics['平均订单金额']:,.0f}",
              delta=None, delta_color="normal")

with col5:
    st.metric("利润率", f"{metrics['利润率']:.1f}%",
              delta=None, delta_color="normal")

st.markdown("---")

# 标签页
tab1, tab2, tab3, tab4 = st.tabs(["📈 趋势分析", "📊 产品分析", "🎯 地区分析", "📋 详细数据"])

# 标签页1:趋势分析
with tab1:
    st.header("销售趋势分析")

    # 销售趋势图
    sales_by_month = filtered_df.groupby('月份')['销售额'].sum().reset_index()
    fig_month = px.line(
        sales_by_month,
        x='月份',
        y='销售额',
        title='月度销售趋势',
        markers=True
    )
    fig_month.update_layout(
        xaxis_title='月份',
        yaxis_title='销售额 (¥)',
        hovermode='x unified'
    )
    st.plotly_chart(fig_month, use_container_width=True)

    # 季度对比
    col1, col2 = st.columns(2)

    with col1:
        sales_by_quarter = filtered_df.groupby('季度')['销售额'].sum().reset_index()
        fig_quarter = px.bar(
            sales_by_quarter,
            x='季度',
            y='销售额',
            title='季度销售额对比',
            color='季度',
            color_discrete_sequence=px.colors.qualitative.Set3
        )
        st.plotly_chart(fig_quarter, use_container_width=True)

    with col2:
        # 利润趋势
        profit_by_month = filtered_df.groupby('月份')['利润'].sum().reset_index()
        fig_profit = px.line(
            profit_by_month,
            x='月份',
            y='利润',
            title='月度利润趋势',
            markers=True,
            color_discrete_sequence=['green']
        )
        st.plotly_chart(fig_profit, use_container_width=True)

    # 下载趋势数据
    csv = sales_by_month.to_csv(index=False)
    st.download_button(
        label="下载月度销售数据",
        data=csv,
        file_name='月度销售数据.csv',
        mime='text/csv'
    )

# 标签页2:产品分析
with tab2:
    st.header("产品销售分析")

    # 销售额排名
    sales_by_product = filtered_df.groupby('产品')['销售额', '利润'].sum().sort_values('销售额', ascending=False)

    # 产品销售额柱状图
    fig_product = px.bar(
        sales_by_product.reset_index(),
        x='产品',
        y='销售额',
        title='产品销售额排名',
        color='销售额',
        color_continuous_scale='Blues'
    )
    fig_product.update_layout(
        xaxis_title='产品',
        yaxis_title='销售额 (¥)',
        xaxis_tickangle=-45
    )
    st.plotly_chart(fig_product, use_container_width=True)

    # 产品利润分析
    fig_profit_product = px.bar(
        sales_by_product.reset_index(),
        x='产品',
        y='利润',
        title='产品利润排名',
        color='利润',
        color_continuous_scale='Greens'
    )
    fig_profit_product.update_layout(
        xaxis_title='产品',
        yaxis_title='利润 (¥)',
        xaxis_tickangle=-45
    )
    st.plotly_chart(fig_profit_product, use_container_width=True)

    # 产品类别分析
    category_sales = filtered_df.groupby('类别')['销售额', '利润'].sum()
    fig_category = px.pie(
        category_sales.reset_index(),
        values='销售额',
        names='类别',
        title='产品类别销售额占比',
        hole=0.3
    )
    st.plotly_chart(fig_category, use_container_width=True)

# 标签页3:地区分析(模拟)
with tab3:
    st.header("地区分析")

    # 由于原始数据没有地区信息,我们添加模拟地区
    # 模拟5个城市
    cities = ['北京', '上海', '广州', '深圳', '杭州']
    np.random.seed(42)  # 确保每次生成相同的模拟数据

    # 为每条记录分配城市
    filtered_df['城市'] = np.random.choice(cities, size=len(filtered_df))

    # 城市销售额
    sales_by_city = filtered_df.groupby('城市')['销售额', '利润'].sum().sort_values('销售额', ascending=False)

    # 城市销售额地图(使用模拟坐标)
    city_coords = {
        '北京': (39.9042, 116.4074),
        '上海': (31.2304, 121.4737),
        '广州': (23.1291, 113.2644),
        '深圳': (22.5431, 114.0579),
        '杭州': (30.2741, 120.1551)
    }

    # 创建地图数据
    map_data = []
    for city in sales_by_city.index:
        lat, lon = city_coords[city]
        map_data.append({
            'city': city,
            'lat': lat,
            'lon': lon,
            'sales': sales_by_city.loc[city, '销售额']
        })

    map_df = pd.DataFrame(map_data)

    # 显示地图
    st.map(
        map_df,
        latitude='lat',
        longitude='lon',
        size='sales',
        zoom=4,
        use_container_width=True
    )

    # 城市排名
    st.subheader("城市销售额排名")
    st.dataframe(sales_by_city.style.format({
        '销售额': '¥{:,.0f}',
        '利润': '¥{:,.0f}'
    }))

# 标签页4:详细数据
with tab4:
    st.header("详细销售数据")

    # 数据展示选项
    show_options = st.multiselect(
        "显示列",
        df.columns.tolist(),
        default=['日期', '产品', '类别', '数量', '单价', '销售额', '利润']
    )

    # 数据表
    if len(show_options) > 0:
        st.dataframe(
            filtered_df[show_options].sort_values('日期', ascending=False),
            use_container_width=True,
            hide_index=True
        )

    # 数据导出
    st.subheader("导出数据")

    export_format = st.selectbox(
        "选择导出格式",
        ["CSV", "Excel"]
    )

    if st.button("导出数据"):
        if export_format == "CSV":
            csv = filtered_df.to_csv(index=False)
            st.download_button(
                label="下载 CSV 文件",
                data=csv,
                file_name='销售数据.csv',
                mime='text/csv'
            )
        else:
            # Excel 导出
            from io import BytesIO
            output = BytesIO()
            with pd.ExcelWriter(output, engine='openpyxl') as writer:
                filtered_df.to_excel(writer, index=False, sheet_name='销售数据')
            excel_data = output.getvalue()
            st.download_button(
                label="下载 Excel 文件",
                data=excel_data,
                file_name='销售数据.xlsx',
                mime='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
            )

# 页脚
st.markdown("---")
st.markdown("💡 提示:使用侧边栏筛选器可以查看特定时间段或类别的数据")

解释

  • 使用缓存机制优化数据加载
  • 实现多维度数据筛选(日期、类别、产品)
  • 创建多个标签页组织不同分析维度
  • 使用 Plotly 创建交互式图表
  • 提供数据导出功能
  • 响应式布局适配不同屏幕

完整代码

项目的完整代码包括:

  1. requirements.txt:定义依赖
  2. generate_data.py:生成模拟数据
  3. app.py:主应用文件

运行和测试

bash 复制代码
# 创建项目目录
mkdir sales-dashboard
cd sales-dashboard

# 创建依赖文件
echo "streamlit>=1.0.0
pandas>=1.0.0
numpy>=1.0.0
plotly>=5.0.0
matplotlib>=3.0.0
seaborn>=0.11.0
openpyxl>=3.0.0" > requirements.txt

# 生成数据
python generate_data.py

# 运行应用
streamlit run app.py

扩展练习

挑战

  1. 添加用户认证功能,不同用户看到不同的数据
  2. 实现数据实时更新功能(连接数据库)
  3. 添加预警功能,当销售异常时发送通知
  4. 实现数据预测功能,使用机器学习模型预测未来销售
  5. 添加数据钻取功能,点击图表查看详细数据

进阶主题

主题1:自定义组件

什么时候需要:需要创建特殊的 UI 组件或集成第三方库。

深入理解

自定义组件允许你扩展 Streamlit 的功能,创建可重用的 UI 元素。你可以使用任何前端框架(React、Vue、Angular)来创建自定义组件。

高级示例

python 复制代码
import streamlit as st
import streamlit.components.v1 as components

# 创建自定义组件
def custom_slider(label, min_value, max_value, value):
    # 使用 HTML 和 JavaScript 创建滑块
    component_value = components.html(
        f"""
        <div id="slider-container">
            <label>{label}</label>
            <input type="range" min="{min_value}" max="{max_value}"
                   value="{value}" id="custom-slider">
            <span id="slider-value">{value}</span>
        </div>
        <script>
            const slider = document.getElementById('custom-slider');
            const valueDisplay = document.getElementById('slider-value');

            slider.addEventListener('input', function() {{
                valueDisplay.textContent = this.value;
                // 通过 Streamlit 的 setComponentValue 更新值
                Streamlit.setComponentValue(this.value);
            }});
        </script>
        <style>
            #slider-container {{
                padding: 10px;
                border: 1px solid #ddd;
                border-radius: 5px;
            }}
            #custom-slider {{
                width: 100%;
            }}
        </style>
        """,
        height=100
    )
    return component_value

# 使用自定义组件
value = custom_slider("自定义滑块", 0, 100, 50)
st.write(f"选择的值: {value}")

性能考虑

  • 自定义组件会增加加载时间
  • 需要处理组件和 Python 之间的数据传递
  • 考虑组件的响应式设计

主题2:多页面应用

什么时候需要:应用需要多个独立页面或视图。

深入理解

Streamlit 1.28+ 支持多页面应用,你可以使用 st.navigation() 创建带有导航栏的多页面应用。

高级示例

python 复制代码
# app.py
import streamlit as st
import streamlit_shadcn as stc

# 创建页面路由
pages = {
    "主页": [
        st.Page("pages/home.py", title="主页", icon="🏠"),
        st.Page("pages/about.py", title="关于", icon="ℹ️"),
    ],
    "数据分析": [
        st.Page("pages/dashboard.py", title="仪表板", icon="📊"),
        st.Page("pages/reports.py", title="报告", icon="📋"),
    ],
    "设置": [
        st.Page("pages/settings.py", title="设置", icon="⚙️"),
    ],
}

# 创建导航
navigation = st.navigation(pages)

# 根据选择的页面导航
if navigation.title == "主页":
    import pages.home as home
    home.run()
elif navigation.title == "关于":
    import pages.about as about
    about.run()
# ... 其他页面

子页面示例(pages/home.py)

python 复制代码
import streamlit as st

def run():
    st.title("欢迎来到数据分析平台")
    st.write("这是一个多页面应用的示例")

    st.subheader("快速开始")
    st.write("选择上方的导航栏开始探索")

    st.subheader("最近活动")
    recent_activities = [
        "创建了销售仪表板",
        "更新了数据源",
        "修复了显示错误"
    ]

    for activity in recent_activities:
        st.write(f"- {activity}")

性能考虑

  • 每个页面都是独立的 Python 脚本
  • 路由会影响应用加载速度
  • 考虑使用缓存减少重复计算

主题3:性能优化

什么时候需要:应用加载慢或运行卡顿。

深入理解

对于大型数据集和复杂计算,需要优化性能以确保良好的用户体验。

高级示例

python 复制代码
import streamlit as st
import pandas as pd
import numpy as np
import time
from functools import lru_cache

# 使用 LRU 缓存
@st.cache_data(max_entries=10, ttl=3600)
def load_large_dataset(filename):
    """加载大型数据集"""
    st.write("正在加载数据集...")
    time.sleep(2)  # 模拟加载时间
    return pd.read_csv(filename)

# 使用性能分析
def profile_function(func):
    """性能装饰器"""
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        execution_time = end_time - start_time
        st.write(f"执行时间: {execution_time:.2f}秒")
        return result
    return wrapper

# 优化后的数据展示
@st.cache_data
def display_optimized_data(df, n_rows=1000):
    """优化的数据展示"""
    # 使用采样减少显示的数据量
    if len(df) > n_rows:
        sample_df = df.sample(n=n_rows, random_state=42)
        st.warning(f"数据量过大,仅显示 {n_rows} 条样本")
        return sample_df
    return df

# 虚拟化数据
@st.cache_data
def get_virtual_data(offset, limit, total):
    """虚拟化数据加载"""
    # 模拟从数据库分页查询
    data = []
    for i in range(offset, min(offset + limit, total)):
        data.append({
            'id': i,
            'value': np.random.rand(),
            'category': f'Category_{i % 10}'
        })
    return pd.DataFrame(data)

# 使用
if st.button("显示数据"):
    # 优化数据展示
    df = load_large_dataset('large_data.csv')
    display_df = display_optimized_data(df)
    st.dataframe(display_df)

    # 虚拟化示例
    offset = st.number_input("偏移量", 0, 10000, 0)
    limit = st.number_input("每页数量", 10, 1000, 100)

    if st.button("获取数据"):
        virtual_df = get_virtual_data(offset, limit, 10000)
        st.dataframe(virtual_df)

性能考虑

  • 合理使用缓存,避免内存泄漏
  • 使用采样减少数据传输量
  • 分页加载大数据集
  • 延迟加载非关键资源
  • 使用异步操作提高响应性

学习资源

官方文档

相关推荐
Kagol17 小时前
TinyVue 支持 Skills 啦!现在你可以让 AI 使用 TinyVue 组件搭建项目
前端·agent·ai编程
柳杉18 小时前
从零打造 AI 全球趋势监测大屏
前端·javascript·aigc
simple_lau18 小时前
Cursor配置MasterGo MCP:一键读取设计稿生成高还原度前端代码
前端·javascript·vue.js
睡不着先生18 小时前
如何设计一个真正可扩展的表单生成器?
前端·javascript·vue.js
天蓝色的鱼鱼18 小时前
模块化与组件化:90%的前端开发者都没搞懂的本质区别
前端·架构·代码规范
明君8799718 小时前
Flutter 如何给图片添加多行文字水印
前端·flutter
leolee1818 小时前
Redux Toolkit 实战使用指南
前端·react.js·redux
bluceli18 小时前
React Hooks最佳实践:写出优雅高效的组件代码
前端·react.js
IT_陈寒18 小时前
JavaScript代码效率提升50%?这5个优化技巧你必须知道!
前端·人工智能·后端
IT_陈寒18 小时前
Java开发必知的5个性能优化黑科技,提升50%效率不是梦!
前端·人工智能·后端