Streamlit 数据处理与可视化
1. 数据加载
1.1 从文件上传
python
import streamlit as st
import pandas as pd
import json
st.title("数据加载")
uploaded_file = st.file_uploader(
"上传数据文件",
type=["csv", "xlsx", "json", "parquet"],
help="支持 CSV、Excel、JSON、Parquet 格式"
)
if uploaded_file:
file_type = uploaded_file.type
if file_type == "text/csv":
# 尝试不同编码
try:
df = pd.read_csv(uploaded_file, encoding="utf-8")
except UnicodeDecodeError:
uploaded_file.seek(0)
df = pd.read_csv(uploaded_file, encoding="gbk")
elif file_type == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
# Excel 文件,可以选择 sheet
xls = pd.ExcelFile(uploaded_file)
sheet_name = st.selectbox("选择工作表", xls.sheet_names)
df = pd.read_excel(uploaded_file, sheet_name=sheet_name)
elif file_type == "application/json":
df = pd.read_json(uploaded_file)
else:
df = pd.read_parquet(uploaded_file)
# 数据预览
st.subheader("数据预览")
tab1, tab2, tab3 = st.tabs(["数据", "统计", "信息"])
with tab1:
n_rows = st.slider("显示行数", 5, 100, 10)
st.dataframe(df.head(n_rows), use_container_width=True)
with tab2:
st.dataframe(df.describe(), use_container_width=True)
with tab3:
st.write(f"行数: {df.shape[0]}")
st.write(f"列数: {df.shape[1]}")
st.write(f"数据类型:")
st.dataframe(df.dtypes.to_frame("类型").T)
# 下载处理后的数据
csv = df.to_csv(index=False).encode("utf-8")
st.download_button(
"下载处理后的数据",
csv,
"processed_data.csv",
"text/csv"
)1.2 从数据库加载
python
import streamlit as st
import pandas as pd
from sqlalchemy import create_engine
# SQLite(简单示例)
@st.cache_resource
def get_sqlite_engine():
return create_engine("sqlite:///data.db")
# PostgreSQL
@st.cache_resource
def get_postgres_engine():
return create_engine(
"postgresql://user:password@localhost:5432/mydb"
)
# MySQL
@st.cache_resource
def get_mysql_engine():
return create_engine(
"mysql+pymysql://user:password@localhost:3306/mydb"
)
# 使用
engine = get_sqlite_engine()
query = st.text_area("输入 SQL 查询", "SELECT * FROM my_table LIMIT 100")
if st.button("执行查询"):
try:
df = pd.read_sql(query, engine)
st.dataframe(df, use_container_width=True)
except Exception as e:
st.error(f"查询失败: {e}")1.3 从 URL 加载
python
import streamlit as st
import pandas as pd
url = st.text_input(
"输入数据URL",
value="https://raw.githubusercontent.com/cs109/2014_data/master/countries.csv"
)
if st.button("加载数据"):
try:
df = pd.read_csv(url)
st.dataframe(df.head(20), use_container_width=True)
st.success(f"成功加载 {len(df)} 行数据")
except Exception as e:
st.error(f"加载失败: {e}")
# 使用内置示例数据集
st.subheader("内置示例数据集")
datasets = {
"Iris": pd.read_csv("https://raw.githubusercontent.com/mwaskom/seaborn-data/master/iris.csv"),
"Tips": pd.read_csv("https://raw.githubusercontent.com/mwaskom/seaborn-data/master/tips.csv"),
"Titanic": pd.read_csv("https://raw.githubusercontent.com/mwaskom/seaborn-data/master/titanic.csv"),
}
selected = st.selectbox("选择数据集", list(datasets.keys()))
if selected:
df = datasets[selected]
st.dataframe(df, use_container_width=True)2. Pandas 集成与动态筛选
2.1 Column Config 列配置
python
import streamlit as st
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
# 创建丰富的示例数据
np.random.seed(42)
n = 100
df = pd.DataFrame({
"姓名": [f"用户{i+1}" for i in range(n)],
"年龄": np.random.randint(18, 65, n),
"收入": np.random.uniform(3000, 30000, n).round(2),
"城市": np.random.choice(["北京", "上海", "广州", "深圳", "杭州", "成都"], n),
"评分": np.random.uniform(1, 5, n).round(1),
"是否活跃": np.random.choice([True, False], n),
"注册日期": pd.date_range("2023-01-01", periods=n, freq="D"),
"头像": [f"https://i.pravatar.cc/100?img={i%70+1}" for i in range(n)],
})
# 高级列配置
st.dataframe(
df,
column_config={
"姓名": st.column_config.TextColumn("姓名", width="medium"),
"年龄": st.column_config.NumberColumn("年龄", format="%d 岁"),
"收入": st.column_config.NumberColumn(
"月收入",
format="¥%.2f",
min_value=0,
),
"城市": st.column_config.SelectboxColumn(
"城市",
options=["北京", "上海", "广州", "深圳", "杭州", "成都"],
),
"评分": st.column_config.ProgressColumn(
"评分",
min_value=0,
max_value=5,
format="%.1f ⭐",
),
"是否活跃": st.column_config.CheckboxColumn("是否活跃"),
"注册日期": st.column_config.DatetimeColumn(
"注册日期",
format="YYYY-MM-DD",
),
"头像": st.column_config.ImageColumn("头像", width="small"),
},
hide_index=True,
use_container_width=True,
num_rows="dynamic", # 允许编辑行数
)2.2 动态筛选器
python
import streamlit as st
import pandas as pd
import numpy as np
# 加载数据
@st.cache_data
def load_data():
np.random.seed(42)
return pd.DataFrame({
"产品": np.random.choice(["笔记本", "手机", "平板", "耳机", "键盘"], 200),
"类别": np.random.choice(["电子", "配件", "外设"], 200),
"价格": np.random.uniform(50, 5000, 200).round(2),
"销量": np.random.randint(1, 100, 200),
"评分": np.random.uniform(1, 5, 200).round(1),
"日期": pd.date_range("2024-01-01", periods=200, freq="D"),
"地区": np.random.choice(["华北", "华东", "华南", "西南", "西北"], 200),
})
df = load_data()
st.title("产品销售数据分析")
# 侧边栏筛选器
with st.sidebar:
st.header("筛选条件")
# 产品多选
products = st.multiselect(
"产品",
df["产品"].unique(),
default=df["产品"].unique()
)
# 类别筛选
categories = st.multiselect(
"类别",
df["类别"].unique(),
default=df["类别"].unique()
)
# 价格范围
price_range = st.slider(
"价格范围",
float(df["价格"].min()),
float(df["价格"].max()),
(float(df["价格"].min()), float(df["价格"].max()))
)
# 评分筛选
min_rating = st.slider("最低评分", 0.0, 5.0, 0.0, 0.1)
# 地区筛选
regions = st.multiselect(
"地区",
df["地区"].unique(),
default=df["地区"].unique()
)
# 应用筛选
filtered_df = df[
(df["产品"].isin(products)) &
(df["类别"].isin(categories)) &
(df["价格"] >= price_range[0]) &
(df["价格"] <= price_range[1]) &
(df["评分"] >= min_rating) &
(df["地区"].isin(regions))
]
# 显示结果
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric("总记录数", len(filtered_df))
with col2:
st.metric("平均价格", f"¥{filtered_df['价格'].mean():.2f}")
with col3:
st.metric("总销量", f"{filtered_df['销量'].sum():,}")
with col4:
st.metric("平均评分", f"{filtered_df['评分'].mean():.1f}")
st.dataframe(filtered_df, use_container_width=True)3. 原生图表
3.1 内置图表类型
python
import streamlit as st
import pandas as pd
import numpy as np
np.random.seed(42)
dates = pd.date_range("2024-01-01", periods=365, freq="D")
data = pd.DataFrame({
"日期": dates,
"销售额": np.random.uniform(1000, 5000, 365).cumsum(),
"访问量": np.random.randint(100, 1000, 365).cumsum(),
"转化率": np.random.uniform(0.01, 0.1, 365),
})
st.subheader("折线图")
st.line_chart(data.set_index("日期")[["销售额", "访问量"]])
st.subheader("面积图")
st.area_chart(data.set_index("日期")[["销售额", "访问量"]])
st.subheader("柱状图")
monthly = data.set_index("日期").resample("M").sum()
st.bar_chart(monthly[["销售额"]])
st.subheader("散点图")
scatter_data = pd.DataFrame({
"x": np.random.randn(200),
"y": np.random.randn(200),
"size": np.random.uniform(10, 100, 200),
})
st.scatter_chart(scatter_data)
st.subheader("地图")
map_data = pd.DataFrame(
np.random.randn(500, 2) / [50, 50] + [37.76, -122.4],
columns=["lat", "lon"]
)
st.map(map_data, color="#FF6B6B", size=20)4. Plotly 可视化
4.1 Plotly Express 快速图表
python
import streamlit as st
import plotly.express as px
import pandas as pd
import numpy as np
# 使用内置数据集
df = px.data.gapminder()
# 散点图
st.subheader("Plotly Express 散点图")
fig = px.scatter(
df.query("year == 2007"),
x="gdpPercap",
y="lifeExp",
size="pop",
color="continent",
hover_name="country",
log_x=True,
size_max=60,
title="2007年 各国GDP与寿命关系"
)
st.plotly_chart(fig, use_container_width=True)
# 动画散点图
st.subheader("动态散点图 (带时间动画)")
fig_anim = px.scatter(
df,
x="gdpPercap",
y="lifeExp",
size="pop",
color="continent",
hover_name="country",
animation_frame="year",
animation_group="country",
log_x=True,
size_max=55,
range_x=[100, 100000],
range_y=[25, 90],
title="各国发展进程 (1952-2007)"
)
st.plotly_chart(fig_anim, use_container_width=True)
# 折线图
st.subheader("折线图")
line_data = df[df["country"].isin(["China", "India", "United States"])]
fig_line = px.line(
line_data,
x="year",
y="gdpPercap",
color="country",
title="中美印GDP变化趋势"
)
st.plotly_chart(fig_line, use_container_width=True)
# 柱状图
st.subheader("柱状图")
bar_data = df[df["year"] == 2007].nlargest(10, "pop")
fig_bar = px.bar(
bar_data,
x="country",
y="pop",
color="continent",
title="2007年人口Top10国家"
)
st.plotly_chart(fig_bar, use_container_width=True)4.2 Plotly Graph Objects 精细控制
python
import streamlit as st
import plotly.graph_objects as go
import numpy as np
import pandas as np
# 创建子图
st.subheader("子图布局")
fig = go.Figure()
# 添加多条线
x = np.linspace(0, 10, 100)
fig.add_trace(go.Scatter(
x=x, y=np.sin(x),
mode="lines",
name="sin(x)",
line=dict(color="blue", width=2)
))
fig.add_trace(go.Scatter(
x=x, y=np.cos(x),
mode="lines+markers",
name="cos(x)",
line=dict(color="red", width=2, dash="dash"),
marker=dict(size=4)
))
fig.update_layout(
title="三角函数图",
xaxis_title="X",
yaxis_title="Y",
template="plotly_white",
hovermode="x unified"
)
st.plotly_chart(fig, use_container_width=True)
# 3D 散点图
st.subheader("3D 散点图")
np.random.seed(42)
n = 100
fig_3d = go.Figure(data=[go.Scatter3d(
x=np.random.randn(n),
y=np.random.randn(n),
z=np.random.randn(n),
mode="markers",
marker=dict(
size=5,
color=np.random.randn(n),
colorscale="Viridis",
opacity=0.8
)
)])
fig_3d.update_layout(
title="3D 散点图",
scene=dict(
xaxis_title="X",
yaxis_title="Y",
zaxis_title="Z"
)
)
st.plotly_chart(fig_3d, use_container_width=True)
# 热力图
st.subheader("热力图")
z = np.random.randn(10, 10)
fig_heat = go.Figure(data=go.Heatmap(
z=z,
colorscale="RdBu_r",
colorbar=dict(title="值")
))
fig_heat.update_layout(title="相关性热力图")
st.plotly_chart(fig_heat, use_container_width=True)
# 饼图
st.subheader("饼图")
fig_pie = go.Figure(data=[go.Pie(
labels=["电子产品", "服装配饰", "食品饮料", "家居用品", "图书文具"],
values=[35, 25, 20, 12, 8],
hole=0.3, # 环形饼图
textinfo="label+percent"
)])
fig_pie.update_layout(title="销售额占比")
st.plotly_chart(fig_pie, use_container_width=True)4.3 Plotly 动画
python
import streamlit as st
import plotly.express as px
import pandas as pd
import numpy as np
# 创建动画数据
np.random.seed(42)
frames_data = []
for year in range(2000, 2024):
df_year = pd.DataFrame({
"x": np.random.randn(50) * (1 + year/100),
"y": np.random.randn(50) * (1 + year/100),
"size": np.random.uniform(10, 100, 50),
"category": np.random.choice(["A", "B", "C"], 50),
"year": year
})
frames_data.append(df_year)
df_anim = pd.concat(frames_data)
# 动画散点图
fig = px.scatter(
df_anim,
x="x",
y="y",
size="size",
color="category",
animation_frame="year",
range_x=[-10, 10],
range_y=[-10, 10],
title="数据分布随时间变化"
)
st.plotly_chart(fig, use_container_width=True)
# 控制播放速度
st.subheader("动画控制")
speed = st.slider("动画速度", 50, 2000, 500)
fig.layout.updatemenus[0].buttons[0].args[1]["frame"]["duration"] = speed
st.plotly_chart(fig, use_container_width=True)5. Altair 可视化
python
import streamlit as st
import altair as alt
import pandas as pd
import numpy as np
# 示例数据
np.random.seed(42)
df = pd.DataFrame({
"x": np.random.randn(200),
"y": np.random.randn(200),
"category": np.random.choice(["A", "B", "C"], 200),
"size": np.random.uniform(10, 100, 200),
"date": pd.date_range("2024-01-01", periods=200, freq="D")
})
# 基本散点图
st.subheader("Altair 散点图")
chart = alt.Chart(df).mark_circle(size=60).encode(
x="x:Q",
y="y:Q",
color="category:N",
tooltip=["x", "y", "category"]
).interactive()
st.altair_chart(chart, use_container_width=True)
# 带交互的柱状图
st.subheader("交互式柱状图")
bar_chart = alt.Chart(df).mark_bar().encode(
x="category:N",
y="count():Q",
color="category:N",
tooltip=["category", "count()"]
).interactive()
st.altair_chart(bar_chart, use_container_width=True)
# 时间序列图
st.subheader("时间序列")
time_df = df.set_index("date").resample("W").size().reset_index(name="count")
line_chart = alt.Chart(time_df).mark_line(
color="steelblue",
strokeWidth=2
).encode(
x="date:T",
y="count:Q",
tooltip=["date", "count"]
).interactive()
st.altair_chart(line_chart, use_container_width=True)
# 热力图
st.subheader("Altair 热力图")
heatmap_data = pd.DataFrame({
"x": list(range(10)) * 10,
"y": [i for i in range(10)] * 10,
"value": np.random.randn(100)
})
heatmap = alt.Chart(heatmap_data).mark_rect().encode(
x="x:O",
y="y:O",
color="value:Q",
tooltip=["x", "y", "value"]
).properties(width=400, height=400)
st.altair_chart(heatmap, use_container_width=True)6. Matplotlib 可视化
python
import streamlit as st
import matplotlib.pyplot as plt
import numpy as np
# 设置中文字体
plt.rcParams["font.sans-serif"] = ["SimHei", "DejaVu Sans"]
plt.rcParams["axes.unicode_minus"] = False
st.subheader("Matplotlib 图表")
# 基本图表
fig, axes = plt.subplots(2, 2, figsize=(12, 8))
# 折线图
x = np.linspace(0, 10, 100)
axes[0, 0].plot(x, np.sin(x), label="sin(x)")
axes[0, 0].plot(x, np.cos(x), label="cos(x)")
axes[0, 0].set_title("三角函数")
axes[0, 0].legend()
# 柱状图
categories = ["A", "B", "C", "D"]
values = [23, 45, 56, 78]
axes[0, 1].bar(categories, values, color=["#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4"])
axes[0, 1].set_title("柱状图")
# 散点图
x = np.random.randn(50)
y = np.random.randn(50)
axes[1, 0].scatter(x, y, c=np.random.rand(50), cmap="viridis", s=100)
axes[1, 0].set_title("散点图")
# 饼图
sizes = [35, 25, 20, 15, 5]
labels = ["电子", "服装", "食品", "家居", "其他"]
axes[1, 1].pie(sizes, labels=labels, autopct="%1.1f%%")
axes[1, 1].set_title("销售占比")
plt.tight_layout()
st.pyplot(fig)
# 3D 图形
st.subheader("Matplotlib 3D 图形")
fig_3d = plt.figure(figsize=(10, 6))
ax = fig_3d.add_subplot(111, projection="3d")
x = np.random.randn(100)
y = np.random.randn(100)
z = np.random.randn(100)
ax.scatter(x, y, z, c=z, cmap="viridis", s=50)
ax.set_xlabel("X")
ax.set_ylabel("Y")
ax.set_zlabel("Z")
ax.set_title("3D 散点图")
st.pyplot(fig_3d)
# 动态图表(使用缓存)
st.subheader("动态图表")
if st.button("生成新数据"):
fig_dynamic, ax = plt.subplots()
x = np.linspace(0, 10, 100)
ax.plot(x, np.random.randn(100).cumsum())
ax.set_title("随机行走")
st.pyplot(fig_dynamic)7. PyDeck 地图可视化
python
import streamlit as st
import pydeck as pdk
import pandas as pd
import numpy as np
# 生成示例数据
np.random.seed(42)
n = 500
map_df = pd.DataFrame({
"lat": np.random.uniform(30, 40, n),
"lon": np.random.uniform(115, 125, n),
"value": np.random.uniform(0, 100, n),
"name": [f"地点{i+1}" for i in range(n)]
})
# 基本散点图层
st.subheader("散点图层")
layer = pdk.Layer(
"ScatterplotLayer",
data=map_df,
get_position=["lon", "lat"],
get_radius=500,
get_fill_color="[255, 107, 107, 160]",
pickable=True,
auto_highlight=True,
)
view = pdk.ViewState(
latitude=35,
longitude=120,
zoom=5,
pitch=0
)
map1 = pdk.Deck(
layers=[layer],
initial_view_state=view,
tooltip={"text": "{name}\n数值: {value:.2f}"}
)
st.pydeck_chart(map1)
# 热力图层
st.subheader("热力图层")
heatmap_layer = pdk.Layer(
"HeatmapLayer",
data=map_df,
get_position=["lon", "lat"],
get_weight="value",
radiusPixels=30,
intensity=1,
threshold=0.05,
)
map2 = pdk.Deck(
layers=[heatmap_layer],
initial_view_state=view,
)
st.pydeck_chart(map2)
# 可视化柱状图层(3D 柱状图)
st.subheader("3D 柱状图")
bar_df = map_df.head(50).copy()
bar_df["height"] = bar_df["value"] * 100
bar_layer = pdk.Layer(
"ColumnLayer",
data=bar_df,
get_position=["lon", "lat"],
get_elevation="height",
elevation_scale=50,
radius=200,
get_fill_color=["255", "107", "107", "160"],
pickable=True,
auto_highlight=True,
)
view_3d = pdk.ViewState(
latitude=35,
longitude=120,
zoom=5,
pitch=45
)
map3 = pdk.Deck(
layers=[bar_layer],
initial_view_state=view_3d,
tooltip={"text": "{name}\n数值: {value:.2f}\n高度: {height:.0f}"}
)
st.pydeck_chart(map3)
# 路径图层
st.subheader("路径图层")
path_df = pd.DataFrame({
"path": [
[[116.4, 39.9], [121.5, 31.2]],
[[116.4, 39.9], [113.3, 23.1]],
[[116.4, 39.9], [104.1, 30.6]],
],
"name": ["北京-上海", "北京-广州", "北京-成都"],
"color": [[255, 0, 0], [0, 255, 0], [0, 0, 255]]
})
path_layer = pdk.Layer(
"PathLayer",
data=path_df,
get_path="path",
get_color="color",
width_min_pixels=3,
get_width=5,
)
map4 = pdk.Deck(
layers=[path_layer],
initial_view_state=view,
)
st.pydeck_chart(map4)8. 实时数据更新与 Fragment
8.1 使用 Fragment 局部刷新
python
import streamlit as st
import pandas as pd
import numpy as np
from datetime import datetime
st.title("实时数据仪表盘")
# 使用 fragment 实现局部刷新(不刷新整个页面)
@st.fragment(run_every="2s") # 每2秒自动刷新
def live_metrics():
"""实时指标 - 独立刷新"""
st.subheader("实时指标")
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric("在线用户", np.random.randint(100, 200))
with col2:
st.metric("CPU使用率", f"{np.random.uniform(20, 80):.1f}%")
with col3:
st.metric("内存使用", f"{np.random.uniform(40, 90):.1f}%")
with col4:
st.metric("响应时间", f"{np.random.uniform(10, 100):.0f}ms")
st.caption(f"最后更新: {datetime.now().strftime('%H:%M:%S')}")
# 使用缓存的 fragment
@st.fragment(run_every="5s")
def live_chart():
"""实时图表 - 独立刷新"""
st.subheader("实时流量")
data = pd.DataFrame({
"时间": pd.date_range(end=datetime.now(), periods=30, freq="1min"),
"请求数": np.random.randint(50, 200, 30),
"响应时间": np.random.uniform(10, 100, 30),
})
st.line_chart(data.set_index("时间")[["请求数"]])
# 静态部分 - 不会被频繁刷新
st.subheader("历史数据")
static_data = pd.DataFrame({
"日期": pd.date_range("2024-01-01", periods=30, freq="D"),
"销售额": np.random.uniform(1000, 5000, 30),
})
st.bar_chart(static_data.set_index("日期"))
# 渲染 fragment
live_metrics()
live_chart()8.2 手动刷新与定时更新
python
import streamlit as st
import time
from datetime import datetime
st.title("数据更新示例")
# 方式1:按钮刷新
if st.button("🔄 立即刷新"):
st.rerun()
# 方式2:自动刷新(使用 st.empty + 循环)
st.subheader("自动刷新(5秒间隔)")
placeholder = st.empty()
for i in range(5): # 只刷新5次作为示例
with placeholder.container():
st.write(f"第 {i+1} 次刷新")
st.write(f"当前时间: {datetime.now().strftime('%H:%M:%S')}")
st.write(f"随机数: {np.random.randint(1, 100)}")
time.sleep(5)
st.success("自动刷新完成")
# 方式3:使用 st.fragment(推荐)
@st.fragment(run_every="3s")
def auto_refresh():
st.write(f"🔄 Fragment 自动刷新: {datetime.now().strftime('%H:%M:%S')}")
auto_refresh()8.3 带回调的数据更新
python
import streamlit as st
import pandas as pd
import numpy as np
st.title("回调式数据更新")
# 初始化 session state
if "data" not in st.session_state:
st.session_state.data = pd.DataFrame({
"x": range(10),
"y": np.random.randn(10)
})
# 更新回调
def update_data():
new_row = pd.DataFrame({
"x": [st.session_state.data["x"].max() + 1],
"y": [np.random.randn()]
})
st.session_state.data = pd.concat(
[st.session_state.data, new_row],
ignore_index=True
)
# 手动添加
col1, col2 = st.columns(2)
with col1:
st.button("添加数据点", on_click=update_data)
with col2:
if st.button("清空数据"):
st.session_state.data = pd.DataFrame({"x": [], "y": []})
# 显示图表
st.line_chart(st.session_state.data.set_index("x"))
st.dataframe(st.session_state.data, use_container_width=True)