Streamlit基础入门与快速原型开发

引言:为什么选择Streamlit?

在当今数据驱动的世界中,快速构建交互式数据应用已成为数据科学家、分析师和开发者的核心需求。传统的Web开发流程(前端HTML/CSS/JavaScript + 后端Python/Node.js + 数据库 + API)虽然功能强大,但对于数据科学项目来说往往过于繁琐。这就是Streamlit诞生的背景。

Streamlit是一个开源的Python库,它允许您用纯Python代码快速创建自定义的Web应用。自2019年发布以来,它已经彻底改变了数据科学家分享其工作的方式。让我们通过以下对比来理解Streamlit的独特价值:

1. Streamlit核心概念解析

1.1 Streamlit的设计哲学

Streamlit的设计理念是"为数据科学而生的最快方式"。它基于以下几个核心原则:

  1. 拥抱Python:所有功能都通过Python API暴露

  2. 即时反馈:每次交互都会重新运行脚本

  3. 简单至上:API设计直观,学习曲线平缓

  4. 开源免费:MIT许可证,社区驱动

1.2 Streamlit的架构模型

理解Streamlit的架构对于高效使用它至关重要:

这种独特的执行模型意味着:每次用户交互都会导致整个脚本重新执行。这看起来可能效率低下,但实际上通过智能的缓存机制,Streamlit能够高效地处理这种模式。

1.3 安装与环境配置

在开始第一个Demo之前,让我们确保环境正确配置。下面是推荐的设置方式:

python 复制代码
# 环境配置检查脚本
# 文件名: check_environment.py
import sys
import subprocess
import pkg_resources

def check_environment():
    """检查Python环境和Streamlit安装"""
    
    print("=" * 50)
    print("环境检查开始")
    print("=" * 50)
    
    # 检查Python版本
    python_version = sys.version_info
    print(f"Python版本: {python_version.major}.{python_version.minor}.{python_version.micro}")
    
    if python_version.major < 3 or (python_version.major == 3 and python_version.minor < 8):
        print("❌ 需要Python 3.8或更高版本")
        return False
    print("✅ Python版本符合要求")
    
    # 检查必要包
    required_packages = ['streamlit', 'pandas', 'numpy', 'plotly']
    
    for package in required_packages:
        try:
            version = pkg_resources.get_distribution(package).version
            print(f"✅ {package}: {version}")
        except pkg_resources.DistributionNotFound:
            print(f"❌ {package}: 未安装")
    
    print("=" * 50)
    print("要安装缺失的包,请运行:")
    print("pip install streamlit pandas numpy plotly matplotlib seaborn")
    print("=" * 50)
    
    return True

if __name__ == "__main__":
    check_environment()

2. Demo 1: Streamlit最简示例

让我们从最简单的Streamlit应用开始,了解其基本结构。

python 复制代码
# demo_hello_world.py
"""
Streamlit最简示例 - 演示基础功能
运行方式: streamlit run demo_hello_world.py
功能特点:
1. 展示Streamlit的核心UI组件
2. 演示交互式控件
3. 展示数据可视化
4. 演示布局系统
"""

import streamlit as st
import pandas as pd
import numpy as np
import time
from datetime import datetime

# ============================
# 1. 页面配置
# ============================
st.set_page_config(
    page_title="Streamlit快速入门演示",
    page_icon="🎯",
    layout="wide",  # 可选 "centered" 或 "wide"
    initial_sidebar_state="expanded"  # 可选 "auto", "expanded", "collapsed"
)

# ============================
# 2. 应用标题和介绍
# ============================
st.title("🎯 Streamlit快速入门")
st.markdown("""
欢迎来到Streamlit世界!这是一个完整的入门示例,展示了Streamlit的核心功能。
从简单的文本展示到复杂的数据可视化,您将在这里找到一切。
""")

# 添加一些装饰
st.divider()

# ============================
# 3. 侧边栏 - 控制面板
# ============================
with st.sidebar:
    st.header("⚙️ 控制面板")
    
    # 主题选择
    theme = st.selectbox(
        "选择主题",
        ["Light", "Dark", "Auto"],
        help="选择应用的主题样式"
    )
    
    # 更新频率
    update_freq = st.slider(
        "数据更新频率(秒)",
        min_value=1,
        max_value=60,
        value=10,
        help="控制数据自动更新的频率"
    )
    
    # 功能开关
    st.subheader("功能开关")
    show_data = st.checkbox("显示示例数据", value=True)
    show_chart = st.checkbox("显示图表", value=True)
    show_metrics = st.checkbox("显示指标", value=True)
    
    # 颜色选择
    st.subheader("图表颜色")
    primary_color = st.color_picker("主色调", "#FF4B4B")
    secondary_color = st.color_picker("辅助色", "#0068C9")
    
    # 信息显示
    st.divider()
    st.info(f"当前时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    
    if st.button("🔄 刷新数据"):
        st.rerun()

# ============================
# 4. 主内容区域
# ============================
# 使用列布局
col1, col2, col3 = st.columns(3)

with col1:
    st.metric("用户数量", "1,234", "+123")
    st.progress(75, text="本月完成度")

with col2:
    st.metric("营收", "¥45,678", "+12.5%")
    st.progress(45, text="季度目标")

with col3:
    st.metric("转化率", "3.45%", "-0.2%")
    st.progress(90, text="用户满意度")

# 添加一些间距
st.divider()

# ============================
# 5. 数据展示部分
# ============================
if show_data:
    st.header("📊 数据展示")
    
    # 创建示例数据
    data_col1, data_col2 = st.columns(2)
    
    with data_col1:
        st.subheader("示例数据表")
        
        # 生成随机数据
        np.random.seed(42)
        data = pd.DataFrame({
            '日期': pd.date_range('2023-01-01', periods=10, freq='D'),
            '销售额': np.random.randint(1000, 5000, 10),
            '利润': np.random.randint(200, 1000, 10),
            '访问量': np.random.randint(100, 1000, 10)
        })
        
        # 添加计算列
        data['利润率'] = (data['利润'] / data['销售额'] * 100).round(2)
        
        # 显示数据表
        st.dataframe(
            data,
            use_container_width=True,
            hide_index=True,
            column_config={
                "销售额": st.column_config.NumberColumn(format="¥%d"),
                "利润": st.column_config.NumberColumn(format="¥%d"),
                "利润率": st.column_config.NumberColumn(format="%.2f%%")
            }
        )
        
        # 数据统计
        with st.expander("📈 数据统计摘要"):
            st.write(data.describe())
    
    with data_col2:
        st.subheader("数据操作")
        
        # 文件上传
        uploaded_file = st.file_uploader(
            "上传数据文件 (CSV/Excel)",
            type=['csv', 'xlsx', 'xls'],
            help="支持CSV和Excel格式"
        )
        
        if uploaded_file is not None:
            try:
                if uploaded_file.name.endswith('.csv'):
                    df_uploaded = pd.read_csv(uploaded_file)
                else:
                    df_uploaded = pd.read_excel(uploaded_file)
                
                st.success(f"成功加载文件: {uploaded_file.name}")
                st.write(f"数据形状: {df_uploaded.shape}")
                st.dataframe(df_uploaded.head(), use_container_width=True)
            except Exception as e:
                st.error(f"读取文件时出错: {str(e)}")
        
        # 数据过滤选项
        st.subheader("数据过滤")
        min_sales = st.slider(
            "最低销售额",
            min_value=int(data['销售额'].min()),
            max_value=int(data['销售额'].max()),
            value=int(data['销售额'].min()),
            step=100
        )
        
        filtered_data = data[data['销售额'] >= min_sales]
        st.write(f"显示 {len(filtered_data)} 条记录 (总计: {len(data)})")

# ============================
# 6. 可视化图表
# ============================
if show_chart:
    st.header("📈 数据可视化")
    
    # 创建示例图表数据
    chart_data = pd.DataFrame({
        '月份': ['1月', '2月', '3月', '4月', '5月', '6月'],
        '产品A': [120, 135, 128, 145, 160, 155],
        '产品B': [90, 95, 110, 105, 115, 120],
        '产品C': [80, 85, 90, 95, 100, 110]
    })
    
    # 图表类型选择
    chart_col1, chart_col2 = st.columns(2)
    
    with chart_col1:
        chart_type = st.selectbox(
            "选择图表类型",
            ["折线图", "柱状图", "面积图", "散点图"]
        )
    
    with chart_col2:
        selected_products = st.multiselect(
            "选择产品",
            ['产品A', '产品B', '产品C'],
            default=['产品A', '产品B']
        )
    
    # 根据选择创建图表
    if selected_products:
        if chart_type == "折线图":
            st.line_chart(
                chart_data.set_index('月份')[selected_products],
                use_container_width=True
            )
        elif chart_type == "柱状图":
            st.bar_chart(
                chart_data.set_index('月份')[selected_products],
                use_container_width=True
            )
        elif chart_type == "面积图":
            st.area_chart(
                chart_data.set_index('月份')[selected_products],
                use_container_width=True
            )
    
    # 更多图表选项
    with st.expander("📊 高级图表选项"):
        # 使用matplotlib创建更多类型的图表
        import matplotlib.pyplot as plt
        
        fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4))
        
        # 饼图
        pie_data = chart_data[selected_products].sum()
        ax1.pie(pie_data, labels=selected_products, autopct='%1.1f%%')
        ax1.set_title('销售额占比')
        
        # 箱线图
        box_data = [chart_data[product] for product in selected_products]
        ax2.boxplot(box_data, labels=selected_products)
        ax2.set_title('销售额分布')
        ax2.set_ylabel('销售额')
        
        st.pyplot(fig)

# ============================
# 7. 交互控件展示
# ============================
st.header("🎮 交互控件")

control_col1, control_col2 = st.columns(2)

with control_col1:
    st.subheader("输入控件")
    
    # 文本输入
    name = st.text_input("请输入您的姓名", "张三")
    
    # 数字输入
    age = st.number_input("年龄", min_value=0, max_value=150, value=25, step=1)
    
    # 选择器
    job = st.selectbox(
        "职业",
        ["学生", "工程师", "设计师", "产品经理", "数据分析师", "其他"]
    )
    
    # 多选
    interests = st.multiselect(
        "兴趣爱好",
        ["编程", "阅读", "运动", "音乐", "旅行", "美食"],
        default=["编程", "阅读"]
    )
    
    # 滑块
    salary = st.slider(
        "期望薪资 (千元)",
        min_value=10,
        max_value=200,
        value=50,
        step=5
    )

with control_col2:
    st.subheader("操作控件")
    
    # 按钮
    if st.button("🎯 提交信息", type="primary"):
        st.success(f"信息已提交!姓名: {name}, 年龄: {age}, 职业: {job}")
        st.write(f"兴趣爱好: {', '.join(interests)}")
        st.write(f"期望薪资: ¥{salary},000")
    
    # 下载按钮
    csv_data = chart_data.to_csv(index=False).encode('utf-8')
    st.download_button(
        label="📥 下载示例数据 (CSV)",
        data=csv_data,
        file_name="sample_data.csv",
        mime="text/csv"
    )
    
    # 单选框
    notification = st.radio(
        "通知方式",
        ["邮箱", "短信", "应用内", "不通知"]
    )
    
    # 开关
    dark_mode = st.toggle("深色模式", value=False)
    
    if dark_mode:
        st.info("已切换到深色模式")
    
    # 进度条
    if st.button("🚀 开始处理"):
        progress_bar = st.progress(0)
        status_text = st.empty()
        
        for i in range(101):
            progress_bar.progress(i)
            status_text.text(f"处理中... {i}%")
            time.sleep(0.01)
        
        status_text.text("✅ 处理完成!")

# ============================
# 8. 表单示例
# ============================
st.header("📝 表单示例")

with st.form("user_form"):
    st.subheader("用户注册表单")
    
    form_col1, form_col2 = st.columns(2)
    
    with form_col1:
        username = st.text_input("用户名*")
        email = st.text_input("邮箱*")
        phone = st.text_input("手机号")
    
    with form_col2:
        password = st.text_input("密码*", type="password")
        confirm_password = st.text_input("确认密码*", type="password")
        country = st.selectbox("国家/地区", ["中国", "美国", "英国", "日本", "其他"])
    
    # 多行文本
    bio = st.text_area("个人简介", placeholder="简单介绍一下自己...")
    
    # 表单提交
    submitted = st.form_submit_button("注册", type="primary")
    
    if submitted:
        if not all([username, email, password, confirm_password]):
            st.error("请填写所有必填项(带*的字段)")
        elif password != confirm_password:
            st.error("两次输入的密码不一致")
        else:
            st.success("注册成功!")
            st.balloons()

# ============================
# 9. 特殊效果和信息展示
# ============================
st.header("✨ 特殊效果")

effect_col1, effect_col2, effect_col3 = st.columns(3)

with effect_col1:
    if st.button("🎈 庆祝一下!"):
        st.balloons()
        st.success("庆祝成功!")

with effect_col2:
    if st.button("❄️ 下雪了!"):
        st.snow()
        st.info("下雪效果激活")

with effect_col3:
    if st.button("🎉 撒花庆祝"):
        st.balloons()
        st.snow()
        st.success("双倍庆祝!")

# 信息展示
st.header("ℹ️ 信息展示")

info_tabs = st.tabs(["成功", "信息", "警告", "错误"])

with info_tabs[0]:
    st.success("操作成功完成!")
    st.success("这是一条成功的消息,用于表示操作成功。")

with info_tabs[1]:
    st.info("这是一条信息提示")
    st.info("Streamlit提供了多种信息展示组件")

with info_tabs[2]:
    st.warning("警告:请注意操作风险")
    st.warning("这是一条警告消息")

with info_tabs[3]:
    st.error("错误:操作失败")
    st.error("这是一条错误消息")

# ============================
# 10. 页脚和附加信息
# ============================
st.divider()
st.caption("""
© 2023 Streamlit入门示例 | 版本 1.0.0 | 
[文档](https://docs.streamlit.io) | 
[GitHub](https://github.com/streamlit) |
[社区论坛](https://discuss.streamlit.io)
""")

# 添加折叠面板显示技术细节
with st.expander("🔧 技术细节"):
    st.code("""
# Streamlit应用的基本结构

1. 导入必要的库
   import streamlit as st
   import pandas as pd
   
2. 页面配置
   st.set_page_config(...)
   
3. 侧边栏
   with st.sidebar:
       # 控制组件
       
4. 主内容区
   st.title("标题")
   st.write("内容")
   
5. 布局组件
   col1, col2 = st.columns(2)
   
6. 交互组件
   st.button(), st.slider(), etc.
   
7. 数据展示
   st.dataframe(), st.table()
   
8. 可视化
   st.line_chart(), st.bar_chart()
    """, language="python")

# 性能监控
performance_expander = st.expander("📊 性能信息")
with performance_expander:
    st.write(f"最后更新: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    st.write(f"数据更新频率: 每{update_freq}秒")
    st.write(f"当前主题: {theme}")
    st.write(f"显示数据: {show_data}")
    st.write(f"显示图表: {show_chart}")

Demo代码结构解析

让我们通过图表来理解这个Demo的代码结构:

3. Demo 2: 交互式数据探索器

第二个Demo展示如何使用Streamlit创建完整的数据分析工具:

python 复制代码
# demo_data_explorer.py
"""
交互式数据探索器 - 完整的数据分析应用
运行方式: streamlit run demo_data_explorer.py
依赖: pip install streamlit pandas numpy plotly matplotlib seaborn
"""

import streamlit as st
import pandas as pd
import numpy as np
import plotly.express as px
import plotly.graph_objects as go
import matplotlib.pyplot as plt
import seaborn as sns
from datetime import datetime, timedelta
import io

# 页面配置
st.set_page_config(
    page_title="交互式数据探索器",
    page_icon="🔍",
    layout="wide"
)

# 应用标题
st.title("🔍 交互式数据探索器")
st.markdown("""
这是一个完整的数据分析工具,支持数据上传、清洗、探索和可视化。
您可以使用内置数据集,也可以上传自己的数据进行探索。
""")

# 侧边栏 - 数据选择和配置
with st.sidebar:
    st.header("📁 数据源配置")
    
    # 数据源选择
    data_source = st.radio(
        "选择数据源",
        ["使用示例数据", "上传CSV文件", "上传Excel文件"],
        index=0
    )
    
    # 文件上传
    uploaded_file = None
    if data_source == "上传CSV文件":
        uploaded_file = st.file_uploader("选择CSV文件", type=['csv'])
    elif data_source == "上传Excel文件":
        uploaded_file = st.file_uploader("选择Excel文件", type=['xlsx', 'xls'])
    
    st.divider()
    
    # 数据处理选项
    st.header("⚙️ 数据处理选项")
    
    # 缺失值处理
    missing_strategy = st.selectbox(
        "缺失值处理",
        ["不处理", "删除包含缺失值的行", "用均值填充", "用中位数填充", "用众数填充"]
    )
    
    # 数据采样
    if st.checkbox("启用数据采样"):
        sample_size = st.slider("采样比例 (%)", 1, 100, 100)
    else:
        sample_size = 100
    
    # 高级选项
    with st.expander("高级选项"):
        encoding = st.selectbox(
            "文件编码",
            ["utf-8", "gbk", "gb2312", "latin1"],
            index=0
        )
        
        delimiter = st.selectbox(
            "分隔符(CSV)",
            [",", ";", "\t", "|"],
            index=0
        )

# 主内容区
tab1, tab2, tab3, tab4, tab5 = st.tabs([
    "📥 数据加载", "🔧 数据清洗", "📊 数据探索", 
    "📈 数据可视化", "📋 报告生成"
])

# Tab 1: 数据加载
with tab1:
    st.header("📥 数据加载与预览")
    
    @st.cache_data
    def load_sample_data():
        """加载示例数据集"""
        # 创建示例销售数据
        np.random.seed(42)
        dates = pd.date_range('2023-01-01', '2023-12-31', freq='D')
        
        data = {
            '日期': dates,
            '产品类别': np.random.choice(['电子产品', '服装', '食品', '家居', '图书'], len(dates)),
            '地区': np.random.choice(['华北', '华东', '华南', '华中', '西南'], len(dates)),
            '销售额': np.random.exponential(1000, len(dates)).cumsum(),
            '利润': np.random.normal(200, 50, len(dates)).cumsum(),
            '订单数量': np.random.poisson(50, len(dates)),
            '客户评分': np.random.uniform(3.0, 5.0, len(dates)),
            '促销活动': np.random.choice([True, False], len(dates), p=[0.3, 0.7]),
            '退货数量': np.random.poisson(2, len(dates))
        }
        
        # 添加一些缺失值用于演示
        for col in ['销售额', '利润', '客户评分']:
            idx = np.random.choice(len(dates), int(len(dates)*0.05), replace=False)
            data[col].iloc[idx] = np.nan
        
        df = pd.DataFrame(data)
        
        # 添加派生特征
        df['月份'] = df['日期'].dt.month
        df['季度'] = df['日期'].dt.quarter
        df['星期'] = df['日期'].dt.day_name()
        df['是否周末'] = df['日期'].dt.dayofweek >= 5
        
        return df
    
    def load_uploaded_data(file, file_type, encoding='utf-8', delimiter=','):
        """加载上传的数据"""
        try:
            if file_type == 'csv':
                df = pd.read_csv(file, encoding=encoding, delimiter=delimiter)
            else:  # excel
                df = pd.read_excel(file)
            
            # 自动检测日期列
            for col in df.columns:
                if df[col].dtype == 'object':
                    try:
                        df[col] = pd.to_datetime(df[col])
                    except:
                        pass
            
            return df, None
        except Exception as e:
            return None, str(e)
    
    # 加载数据
    if data_source == "使用示例数据":
        df = load_sample_data()
        st.success("✅ 已加载示例数据集")
    elif uploaded_file is not None:
        file_type = 'csv' if data_source == "上传CSV文件" else 'excel'
        df, error = load_uploaded_data(
            uploaded_file, 
            file_type, 
            encoding=encoding, 
            delimiter=delimiter
        )
        
        if error:
            st.error(f"❌ 加载文件时出错: {error}")
            st.stop()
        else:
            st.success(f"✅ 已加载文件: {uploaded_file.name}")
    else:
        st.info("请上传文件或使用示例数据")
        st.stop()
    
    # 数据采样
    if sample_size < 100:
        df = df.sample(frac=sample_size/100, random_state=42)
        st.info(f"📊 已对数据进行采样: {sample_size}%")
    
    # 显示数据基本信息
    col1, col2, col3, col4 = st.columns(4)
    
    with col1:
        st.metric("数据行数", len(df))
    
    with col2:
        st.metric("数据列数", len(df.columns))
    
    with col3:
        missing_values = df.isnull().sum().sum()
        st.metric("缺失值数量", missing_values)
    
    with col4:
        st.metric("内存使用", f"{df.memory_usage(deep=True).sum() / 1024**2:.2f} MB")
    
    st.divider()
    
    # 数据显示选项
    display_options = st.columns(3)
    
    with display_options[0]:
        show_rows = st.slider("显示行数", 5, 100, 10)
    
    with display_options[1]:
        start_row = st.number_input("起始行", 0, len(df)-show_rows, 0)
    
    with display_options[2]:
        sort_by = st.selectbox("排序字段", ['无'] + list(df.columns))
    
    # 处理排序
    display_df = df.iloc[start_row:start_row+show_rows]
    if sort_by != '无':
        display_df = display_df.sort_values(sort_by)
    
    # 显示数据
    st.subheader("数据预览")
    st.dataframe(display_df, use_container_width=True)
    
    # 数据信息
    with st.expander("📋 数据详细信息"):
        st.subheader("数据类型")
        dtype_df = pd.DataFrame({
            '列名': df.columns,
            '数据类型': df.dtypes.astype(str).values,
            '非空数量': df.count().values,
            '空值数量': df.isnull().sum().values,
            '空值比例': (df.isnull().sum() / len(df) * 100).round(2).values
        })
        st.dataframe(dtype_df, use_container_width=True)
        
        st.subheader("内存使用详情")
        memory_df = pd.DataFrame({
            '列名': df.columns,
            '内存(MB)': (df.memory_usage(deep=True) / 1024**2).round(4).values
        }).sort_values('内存(MB)', ascending=False)
        st.dataframe(memory_df, use_container_width=True)

# Tab 2: 数据清洗
with tab2:
    st.header("🔧 数据清洗与预处理")
    
    if 'df' not in locals():
        st.warning("请先加载数据")
        st.stop()
    
    # 创建数据清洗副本
    df_clean = df.copy()
    
    # 列选择
    st.subheader("列选择")
    selected_columns = st.multiselect(
        "选择要保留的列",
        df_clean.columns.tolist(),
        default=df_clean.columns.tolist()
    )
    
    if selected_columns:
        df_clean = df_clean[selected_columns]
    
    # 缺失值处理
    st.subheader("缺失值处理")
    
    if missing_strategy != "不处理":
        missing_before = df_clean.isnull().sum().sum()
        
        if missing_strategy == "删除包含缺失值的行":
            df_clean = df_clean.dropna()
        elif missing_strategy == "用均值填充":
            numeric_cols = df_clean.select_dtypes(include=[np.number]).columns
            df_clean[numeric_cols] = df_clean[numeric_cols].fillna(df_clean[numeric_cols].mean())
        elif missing_strategy == "用中位数填充":
            numeric_cols = df_clean.select_dtypes(include=[np.number]).columns
            df_clean[numeric_cols] = df_clean[numeric_cols].fillna(df_clean[numeric_cols].median())
        elif missing_strategy == "用众数填充":
            for col in df_clean.columns:
                if df_clean[col].dtype == 'object' or df_clean[col].dtype.name == 'category':
                    mode_val = df_clean[col].mode()[0] if not df_clean[col].mode().empty else None
                    df_clean[col] = df_clean[col].fillna(mode_val)
        
        missing_after = df_clean.isnull().sum().sum()
        st.info(f"处理前缺失值: {missing_before}, 处理后缺失值: {missing_after}")
    
    # 异常值检测
    st.subheader("异常值检测与处理")
    
    outlier_col = st.selectbox(
        "选择要检测异常值的数值列",
        df_clean.select_dtypes(include=[np.number]).columns.tolist()
    )
    
    if outlier_col:
        Q1 = df_clean[outlier_col].quantile(0.25)
        Q3 = df_clean[outlier_col].quantile(0.75)
        IQR = Q3 - Q1
        lower_bound = Q1 - 1.5 * IQR
        upper_bound = Q3 + 1.5 * IQR
        
        outliers = df_clean[(df_clean[outlier_col] < lower_bound) | (df_clean[outlier_col] > upper_bound)]
        
        col1, col2, col3, col4 = st.columns(4)
        
        with col1:
            st.metric("Q1 (25%)", f"{Q1:.2f}")
        
        with col2:
            st.metric("Q3 (75%)", f"{Q3:.2f}")
        
        with col3:
            st.metric("IQR", f"{IQR:.2f}")
        
        with col4:
            st.metric("异常值数量", len(outliers))
        
        # 异常值处理选项
        if len(outliers) > 0:
            outlier_strategy = st.selectbox(
                "异常值处理策略",
                ["不处理", "删除异常值", "用边界值替换"]
            )
            
            if outlier_strategy == "删除异常值":
                df_clean = df_clean[~df_clean.index.isin(outliers.index)]
                st.success(f"已删除 {len(outliers)} 个异常值")
            elif outlier_strategy == "用边界值替换":
                df_clean.loc[df_clean[outlier_col] < lower_bound, outlier_col] = lower_bound
                df_clean.loc[df_clean[outlier_col] > upper_bound, outlier_col] = upper_bound
                st.success("异常值已用边界值替换")
    
    # 数据类型转换
    st.subheader("数据类型转换")
    
    type_col1, type_col2 = st.columns(2)
    
    with type_col1:
        columns_to_convert = st.multiselect(
            "选择要转换的列",
            df_clean.columns.tolist()
        )
    
    with type_col2:
        target_type = st.selectbox(
            "目标类型",
            ["数值型", "字符型", "日期型", "分类型"]
        )
    
    if st.button("应用类型转换") and columns_to_convert:
        for col in columns_to_convert:
            try:
                if target_type == "数值型":
                    df_clean[col] = pd.to_numeric(df_clean[col], errors='coerce')
                elif target_type == "字符型":
                    df_clean[col] = df_clean[col].astype(str)
                elif target_type == "日期型":
                    df_clean[col] = pd.to_datetime(df_clean[col], errors='coerce')
                elif target_type == "分类型":
                    df_clean[col] = df_clean[col].astype('category')
                st.success(f"列 '{col}' 已转换为 {target_type}")
            except Exception as e:
                st.error(f"转换列 '{col}' 时出错: {str(e)}")
    
    # 显示清洗后的数据
    st.subheader("清洗后数据预览")
    st.dataframe(df_clean.head(), use_container_width=True)
    
    # 数据清洗统计
    with st.expander("📊 清洗统计"):
        st.write(f"原始数据形状: {df.shape}")
        st.write(f"清洗后数据形状: {df_clean.shape}")
        st.write(f"删除的行数: {len(df) - len(df_clean)}")
        st.write(f"删除的列数: {len(df.columns) - len(df_clean.columns)}")

# Tab 3: 数据探索
with tab3:
    st.header("📊 数据探索性分析")
    
    if 'df_clean' not in locals():
        df_clean = df
    
    # 描述性统计
    st.subheader("描述性统计")
    
    stat_col1, stat_col2, stat_col3 = st.columns(3)
    
    with stat_col1:
        stat_type = st.selectbox(
            "统计类型",
            ["所有列", "仅数值列", "仅分类列"]
        )
    
    with stat_col2:
        percentiles = st.multiselect(
            "百分位数",
            ["25%", "50%", "75%", "90%", "95%", "99%"],
            default=["25%", "50%", "75%"]
        )
    
    with stat_col3:
        if st.button("重新计算统计"):
            st.rerun()
    
    # 计算统计
    if stat_type == "所有列":
        stats_df = df_clean.describe(include='all', percentiles=[p/100 for p in [25, 50, 75, 90, 95, 99]])
    elif stat_type == "仅数值列":
        stats_df = df_clean.describe(include=[np.number], percentiles=[p/100 for p in [25, 50, 75, 90, 95, 99]])
    else:
        stats_df = df_clean.describe(include=['object', 'category'])
    
    st.dataframe(stats_df, use_container_width=True)
    
    # 相关性分析
    st.subheader("相关性分析")
    
    numeric_cols = df_clean.select_dtypes(include=[np.number]).columns
    
    if len(numeric_cols) >= 2:
        corr_method = st.selectbox(
            "相关性计算方法",
            ["皮尔逊相关系数", "斯皮尔曼相关系数", "肯德尔相关系数"]
        )
        
        if corr_method == "皮尔逊相关系数":
            corr_matrix = df_clean[numeric_cols].corr(method='pearson')
        elif corr_method == "斯皮尔曼相关系数":
            corr_matrix = df_clean[numeric_cols].corr(method='spearman')
        else:
            corr_matrix = df_clean[numeric_cols].corr(method='kendall')
        
        # 显示相关性矩阵
        st.write("相关性矩阵:")
        st.dataframe(corr_matrix.style.background_gradient(cmap='coolwarm', axis=None), 
                    use_container_width=True)
        
        # 相关性热图
        fig, ax = plt.subplots(figsize=(10, 8))
        sns.heatmap(corr_matrix, annot=True, fmt='.2f', cmap='coolwarm', 
                   center=0, square=True, ax=ax)
        ax.set_title(f'相关性热图 ({corr_method})')
        st.pyplot(fig)
    else:
        st.warning("数值列不足,无法计算相关性")
    
    # 分布分析
    st.subheader("分布分析")
    
    dist_col = st.selectbox(
        "选择要分析的列",
        numeric_cols if len(numeric_cols) > 0 else df_clean.columns
    )
    
    if dist_col in df_clean.columns:
        col1, col2 = st.columns(2)
        
        with col1:
            # 直方图
            fig1, ax1 = plt.subplots(figsize=(8, 6))
            df_clean[dist_col].hist(bins=30, ax=ax1, edgecolor='black')
            ax1.set_title(f'{dist_col} 分布直方图')
            ax1.set_xlabel(dist_col)
            ax1.set_ylabel('频数')
            st.pyplot(fig1)
        
        with col2:
            # 箱线图
            fig2, ax2 = plt.subplots(figsize=(8, 6))
            df_clean[[dist_col]].boxplot(ax=ax2)
            ax2.set_title(f'{dist_col} 箱线图')
            ax2.set_ylabel(dist_col)
            st.pyplot(fig2)
    
    # 分组分析
    st.subheader("分组分析")
    
    group_cols = df_clean.select_dtypes(include=['object', 'category']).columns.tolist()
    value_cols = df_clean.select_dtypes(include=[np.number]).columns.tolist()
    
    if group_cols and value_cols:
        group_by = st.selectbox("分组字段", group_cols)
        agg_col = st.selectbox("聚合字段", value_cols)
        agg_func = st.selectbox("聚合函数", ["均值", "总和", "中位数", "标准差", "最小值", "最大值"])
        
        func_map = {
            "均值": "mean",
            "总和": "sum", 
            "中位数": "median",
            "标准差": "std",
            "最小值": "min",
            "最大值": "max"
        }
        
        grouped = df_clean.groupby(group_by)[agg_col].agg(func_map[agg_func]).reset_index()
        grouped = grouped.sort_values(agg_col, ascending=False)
        
        col1, col2 = st.columns(2)
        
        with col1:
            st.write(f"按 {group_by} 分组 - {agg_func}")
            st.dataframe(grouped, use_container_width=True)
        
        with col2:
            fig, ax = plt.subplots(figsize=(10, 6))
            bars = ax.bar(grouped[group_by].astype(str), grouped[agg_col])
            ax.set_title(f'{agg_col} 按 {group_by} 分组 ({agg_func})')
            ax.set_xlabel(group_by)
            ax.set_ylabel(agg_col)
            ax.tick_params(axis='x', rotation=45)
            
            # 添加数值标签
            for bar in bars:
                height = bar.get_height()
                ax.text(bar.get_x() + bar.get_width()/2., height,
                       f'{height:.2f}', ha='center', va='bottom')
            
            st.pyplot(fig)

# Tab 4: 数据可视化
with tab4:
    st.header("📈 高级数据可视化")
    
    if 'df_clean' not in locals():
        df_clean = df
    
    # 可视化类型选择
    viz_type = st.selectbox(
        "可视化类型",
        ["散点图", "折线图", "柱状图", "箱线图", "热力图", "分布图", "相关性矩阵"]
    )
    
    if viz_type == "散点图":
        col1, col2, col3 = st.columns(3)
        
        with col1:
            x_axis = st.selectbox("X轴", numeric_cols)
        with col2:
            y_axis = st.selectbox("Y轴", numeric_cols)
        with col3:
            color_by = st.selectbox("颜色分组", ["无"] + group_cols)
        
        if x_axis and y_axis:
            if color_by == "无":
                fig = px.scatter(df_clean, x=x_axis, y=y_axis, 
                               title=f'{y_axis} vs {x_axis}')
            else:
                fig = px.scatter(df_clean, x=x_axis, y=y_axis, color=color_by,
                               title=f'{y_axis} vs {x_axis} (按{color_by}分组)')
            st.plotly_chart(fig, use_container_width=True)
    
    elif viz_type == "折线图":
        if '日期' in df_clean.columns:
            time_cols = ['日期']
        else:
            time_cols = df_clean.select_dtypes(include=['datetime64']).columns.tolist()
        
        if time_cols:
            time_col = st.selectbox("时间列", time_cols)
            value_col = st.selectbox("数值列", numeric_cols)
            group_col = st.selectbox("分组列(可选)", ["无"] + group_cols)
            
            if time_col and value_col:
                if group_col == "无":
                    fig = px.line(df_clean, x=time_col, y=value_col,
                                title=f'{value_col} 随时间变化')
                else:
                    fig = px.line(df_clean, x=time_col, y=value_col, color=group_col,
                                title=f'{value_col} 随时间变化 (按{group_col}分组)')
                st.plotly_chart(fig, use_container_width=True)
        else:
            st.warning("未找到日期时间列")
    
    elif viz_type == "柱状图":
        col1, col2, col3 = st.columns(3)
        
        with col1:
            x_axis = st.selectbox("X轴(分类)", ["无"] + group_cols)
        with col2:
            y_axis = st.selectbox("Y轴(数值)", numeric_cols)
        with col3:
            agg_func = st.selectbox("聚合函数", ["计数", "求和", "平均值"])
        
        if x_axis != "无" and y_axis:
            if agg_func == "计数":
                agg_data = df_clean.groupby(x_axis).size().reset_index(name='计数')
                y_col = '计数'
            else:
                func = "sum" if agg_func == "求和" else "mean"
                agg_data = df_clean.groupby(x_axis)[y_axis].agg(func).reset_index()
                y_col = y_axis
            
            fig = px.bar(agg_data, x=x_axis, y=y_col,
                        title=f'{agg_func}柱状图')
            st.plotly_chart(fig, use_container_width=True)
    
    # 更多可视化选项...
    # 由于篇幅限制,这里省略了其他可视化类型的代码

# Tab 5: 报告生成
with tab5:
    st.header("📋 分析报告生成")
    
    # 报告配置
    st.subheader("报告配置")
    
    report_col1, report_col2 = st.columns(2)
    
    with report_col1:
        report_title = st.text_input("报告标题", "数据分析报告")
        include_summary = st.checkbox("包含摘要", True)
        include_statistics = st.checkbox("包含统计信息", True)
        include_visualizations = st.checkbox("包含可视化", True)
    
    with report_col2:
        report_format = st.selectbox("报告格式", ["HTML", "Markdown", "Text"])
        include_recommendations = st.checkbox("包含建议", True)
        auto_analyze = st.checkbox("自动分析", False)
    
    # 生成报告
    if st.button("生成分析报告", type="primary"):
        with st.spinner("正在生成报告..."):
            # 模拟报告生成
            time.sleep(2)
            
            # 创建报告内容
            report_content = []
            
            if report_format == "HTML":
                report_content.append(f"<h1>{report_title}</h1>")
                report_content.append(f"<p>生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>")
                report_content.append(f"<p>数据行数: {len(df_clean) if 'df_clean' in locals() else len(df)}</p>")
                report_content.append(f"<p>数据列数: {len(df_clean.columns) if 'df_clean' in locals() else len(df.columns)}</p>")
                
                if include_summary:
                    report_content.append("<h2>数据摘要</h2>")
                    report_content.append("<p>这是一份自动生成的数据分析报告。</p>")
                
                if include_statistics:
                    report_content.append("<h2>统计信息</h2>")
                    # 添加统计信息...
                
                if include_recommendations:
                    report_content.append("<h2>建议</h2>")
                    report_content.append("<ul>")
                    report_content.append("<li>建议定期清理数据中的缺失值</li>")
                    report_content.append("<li>考虑对数值列进行标准化处理</li>")
                    report_content.append("<li>建议监控异常值对分析结果的影响</li>")
                    report_content.append("</ul>")
            
            elif report_format == "Markdown":
                report_content.append(f"# {report_title}\n")
                report_content.append(f"**生成时间**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
                report_content.append(f"**数据行数**: {len(df_clean) if 'df_clean' in locals() else len(df)}\n")
                report_content.append(f"**数据列数**: {len(df_clean.columns) if 'df_clean' in locals() else len(df.columns)}\n")
                
                if include_summary:
                    report_content.append("\n## 数据摘要\n")
                    report_content.append("这是一份自动生成的数据分析报告。\n")
            
            else:  # Text
                report_content.append(f"{report_title}\n")
                report_content.append("=" * 50)
                report_content.append(f"生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
                report_content.append(f"数据行数: {len(df_clean) if 'df_clean' in locals() else len(df)}")
                report_content.append(f"数据列数: {len(df_clean.columns) if 'df_clean' in locals() else len(df.columns)}")
            
            # 显示报告
            st.subheader("生成的分析报告")
            report_text = "\n".join(report_content)
            st.text_area("报告内容", report_text, height=300)
            
            # 下载按钮
            if report_format == "HTML":
                mime_type = "text/html"
                file_ext = "html"
            elif report_format == "Markdown":
                mime_type = "text/markdown"
                file_ext = "md"
            else:
                mime_type = "text/plain"
                file_ext = "txt"
            
            st.download_button(
                label=f"📥 下载报告 (.{file_ext})",
                data=report_text,
                file_name=f"data_analysis_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.{file_ext}",
                mime=mime_type
            )

# 页脚
st.divider()
st.caption("""
交互式数据探索器 | 版本 1.0.0 | 
使用Streamlit构建 | 数据仅供参考
""")

# 侧边栏底部信息
with st.sidebar:
    st.divider()
    st.info("""
    **使用提示:**
    1. 从"数据加载"标签页开始
    2. 在"数据清洗"中处理数据质量问题
    3. 使用"数据探索"了解数据特征
    4. 在"数据可视化"中创建图表
    5. 最后生成分析报告
    """)

应用架构解析

这个复杂应用的架构如下:

4. Streamlit核心概念深度解析

4.1 Streamlit的执行模型

理解Streamlit的执行模型是掌握其核心的关键。让我们通过一个简单的例子来说明:

python 复制代码
# execution_model.py
"""
Streamlit执行模型演示
展示每次交互如何导致脚本重新执行
"""

import streamlit as st
import time

st.title("Streamlit执行模型演示")

# 初始化计数器
if 'counter' not in st.session_state:
    st.session_state.counter = 0

# 显示当前计数
st.write(f"当前计数: {st.session_state.counter}")

# 显示脚本执行次数
if 'execution_count' not in st.session_state:
    st.session_state.execution_count = 0

st.session_state.execution_count += 1
st.write(f"脚本执行次数: {st.session_state.execution_count}")

# 交互按钮
col1, col2, col3 = st.columns(3)

with col1:
    if st.button("增加计数"):
        st.session_state.counter += 1
        st.rerun()  # 手动触发重新执行

with col2:
    if st.button("减少计数"):
        st.session_state.counter -= 1
        st.rerun()

with col3:
    if st.button("重置计数"):
        st.session_state.counter = 0
        st.rerun()

# 显示执行时间戳
st.write(f"最后执行时间: {time.ctime()}")

# 演示Streamlit的执行流程
st.divider()
st.subheader("执行流程说明")

st.markdown("""
**Streamlit执行流程:**
1. 用户与界面交互(如点击按钮)
2. 触发脚本重新执行
3. 从头到尾运行整个脚本
4. 更新界面状态
5. 等待下一次交互

**关键特点:**
- 每次交互都重新运行脚本
- 使用`st.session_state`保持状态
- 通过缓存避免重复计算
- 自动管理界面更新
""")

# 演示缓存
@st.cache_data
def expensive_computation(n):
    """模拟耗时计算"""
    time.sleep(2)  # 模拟2秒计算
    return n * 2

if st.button("执行耗时计算"):
    with st.spinner("计算中..."):
        result = expensive_computation(10)
        st.success(f"计算结果: {result}")

4.2 会话状态(Session State)深度解析

会话状态是Streamlit中管理状态的核心机制。让我们通过一个更复杂的例子来理解:

python 复制代码
# session_state_demo.py
"""
Streamlit会话状态深度解析
展示不同状态管理模式的对比
"""

import streamlit as st
import json
import pickle
import hashlib
from datetime import datetime

st.set_page_config(
    page_title="会话状态演示",
    page_icon="💾",
    layout="wide"
)

st.title("💾 Streamlit会话状态深度解析")

# 标签页
tab1, tab2, tab3, tab4 = st.tabs([
    "基础使用", "状态同步", "高级模式", "状态持久化"
])

# Tab 1: 基础使用
with tab1:
    st.header("1. 会话状态基础")
    
    st.markdown("""
    **会话状态的核心概念:**
    - 跨交互保持数据
    - 类似于Python字典
    - 键值对存储
    - 自动序列化/反序列化
    """)
    
    # 初始化状态
    if 'user_info' not in st.session_state:
        st.session_state.user_info = {
            'name': '张三',
            'age': 25,
            'preferences': []
        }
    
    if 'history' not in st.session_state:
        st.session_state.history = []
    
    # 状态操作
    col1, col2 = st.columns(2)
    
    with col1:
        st.subheader("当前状态")
        st.json(st.session_state)
        
        # 状态信息
        st.metric("状态键数量", len(st.session_state))
        st.metric("历史记录数", len(st.session_state.history))
    
    with col2:
        st.subheader("状态操作")
        
        # 修改状态
        new_name = st.text_input("修改姓名", st.session_state.user_info['name'])
        new_age = st.number_input("修改年龄", 
                                 min_value=0, max_value=150, 
                                 value=st.session_state.user_info['age'])
        
        if st.button("更新用户信息"):
            st.session_state.user_info['name'] = new_name
            st.session_state.user_info['age'] = new_age
            st.session_state.history.append({
                'action': 'update_user_info',
                'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
                'old_name': st.session_state.user_info['name'],
                'new_name': new_name
            })
            st.success("用户信息已更新")
            st.rerun()
        
        # 添加偏好
        preference = st.text_input("添加偏好")
        if st.button("添加偏好") and preference:
            if preference not in st.session_state.user_info['preferences']:
                st.session_state.user_info['preferences'].append(preference)
                st.session_state.history.append({
                    'action': 'add_preference',
                    'preference': preference
                })
                st.success(f"偏好 '{preference}' 已添加")
                st.rerun()
        
        # 清空状态
        if st.button("清空历史", type="secondary"):
            st.session_state.history = []
            st.success("历史已清空")
            st.rerun()

# Tab 2: 状态同步
with tab2:
    st.header("2. 状态同步机制")
    
    st.markdown("""
    **状态同步的挑战:**
    - 多个组件访问同一状态
    - 状态的更新顺序
    - 避免竞态条件
    """)
    
    # 演示状态同步问题
    if 'shared_counter' not in st.session_state:
        st.session_state.shared_counter = 0
    
    if 'last_updated_by' not in st.session_state:
        st.session_state.last_updated_by = None
    
    st.subheader("共享计数器示例")
    
    counter_col1, counter_col2, counter_col3 = st.columns(3)
    
    with counter_col1:
        st.metric("当前值", st.session_state.shared_counter)
        st.write(f"最后更新: {st.session_state.last_updated_by or '无'}")
    
    with counter_col2:
        if st.button("按钮A: +1", key="btn_a"):
            st.session_state.shared_counter += 1
            st.session_state.last_updated_by = "按钮A"
            st.rerun()
        
        if st.button("按钮A: +5", key="btn_a5"):
            st.session_state.shared_counter += 5
            st.session_state.last_updated_by = "按钮A"
            st.rerun()
    
    with counter_col3:
        if st.button("按钮B: +1", key="btn_b"):
            st.session_state.shared_counter += 1
            st.session_state.last_updated_by = "按钮B"
            st.rerun()
        
        if st.button("按钮B: -1", key="btn_b_minus"):
            st.session_state.shared_counter -= 1
            st.session_state.last_updated_by = "按钮B"
            st.rerun()
    
    # 状态更新历史
    st.subheader("状态更新历史")
    
    if 'update_history' not in st.session_state:
        st.session_state.update_history = []
    
    # 显示历史
    for i, update in enumerate(st.session_state.update_history[-10:]):  # 显示最后10条
        st.write(f"{i+1}. {update}")
    
    # 添加历史记录
    if st.button("记录当前状态"):
        st.session_state.update_history.append({
            'counter': st.session_state.shared_counter,
            'updated_by': st.session_state.last_updated_by,
            'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        })
        st.rerun()

# Tab 3: 高级模式
with tab3:
    st.header("3. 高级状态模式")
    
    # 使用回调函数
    st.subheader("回调函数模式")
    
    def increment_counter(key):
        """计数器增加回调"""
        st.session_state[key] = st.session_state.get(key, 0) + 1
    
    def decrement_counter(key):
        """计数器减少回调"""
        st.session_state[key] = st.session_state.get(key, 0) - 1
    
    def reset_counter(key):
        """计数器重置回调"""
        st.session_state[key] = 0
    
    # 使用回调的计数器
    counter_key = "advanced_counter"
    
    if counter_key not in st.session_state:
        st.session_state[counter_key] = 0
    
    col1, col2, col3 = st.columns(3)
    
    with col1:
        st.number_input(
            "高级计数器",
            value=st.session_state[counter_key],
            key=counter_key
        )
    
    with col2:
        st.button(
            "增加",
            on_click=increment_counter,
            args=(counter_key,)
        )
        
        st.button(
            "减少", 
            on_click=decrement_counter,
            args=(counter_key,)
        )
    
    with col3:
        st.button(
            "重置",
            on_click=reset_counter,
            args=(counter_key,),
            type="secondary"
        )
    
    # 状态验证
    st.subheader("状态验证")
    
    def validate_age(age):
        """年龄验证"""
        if age < 0 or age > 150:
            return False, "年龄必须在0-150之间"
        return True, ""
    
    if 'validated_age' not in st.session_state:
        st.session_state.validated_age = 25
        st.session_state.age_error = ""
    
    age = st.number_input(
        "年龄(带验证)",
        min_value=0,
        max_value=150,
        value=st.session_state.validated_age,
        key="age_input"
    )
    
    if st.button("验证年龄"):
        is_valid, error_msg = validate_age(age)
        if is_valid:
            st.session_state.validated_age = age
            st.session_state.age_error = ""
            st.success("年龄验证通过")
        else:
            st.session_state.age_error = error_msg
            st.error(error_msg)
    
    if st.session_state.age_error:
        st.error(f"验证错误: {st.session_state.age_error}")

# Tab 4: 状态持久化
with tab4:
    st.header("4. 状态持久化")
    
    st.markdown("""
    **状态持久化的应用场景:**
    - 用户偏好设置
    - 表单数据保存
    - 复杂计算中间结果
    - 跨会话数据共享
    """)
    
    # 初始化持久化状态
    if 'persistent_data' not in st.session_state:
        st.session_state.persistent_data = {
            'user_settings': {},
            'form_data': {},
            'cached_results': {}
        }
    
    if 'auto_save' not in st.session_state:
        st.session_state.auto_save = False
    
    # 持久化方法选择
    st.subheader("持久化方法")
    
    persistence_method = st.radio(
        "选择持久化方法",
        ["会话内持久化", "浏览器存储", "文件存储", "数据库存储"],
        horizontal=True
    )
    
    # 1. 会话内持久化演示
    if persistence_method == "会话内持久化":
        st.info("""
        会话内持久化仅在当前浏览器标签页有效。
        刷新页面或关闭标签页后数据将丢失。
        """)
        
        if 'session_data' not in st.session_state:
            st.session_state.session_data = {
                'visit_count': 0,
                'last_visit': None,
                'pages_visited': []
            }
        
        # 更新会话数据
        st.session_state.session_data['visit_count'] += 1
        st.session_state.session_data['last_visit'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        
        # 添加当前页面
        current_page = st.text_input("当前页面", "状态持久化演示")
        if current_page and current_page not in st.session_state.session_data['pages_visited']:
            st.session_state.session_data['pages_visited'].append(current_page)
        
        # 显示会话数据
        st.json(st.session_state.session_data)
        
        # 会话操作
        col1, col2 = st.columns(2)
        with col1:
            if st.button("清空访问记录"):
                st.session_state.session_data['pages_visited'] = []
                st.success("访问记录已清空")
                st.rerun()
        
        with col2:
            if st.button("重置会话数据"):
                st.session_state.session_data = {
                    'visit_count': 0,
                    'last_visit': None,
                    'pages_visited': []
                }
                st.success("会话数据已重置")
                st.rerun()
    
    # 2. 浏览器存储模拟
    elif persistence_method == "浏览器存储":
        st.warning("注意:Streamlit本身不提供浏览器存储API,这里通过模拟展示概念")
        
        # 模拟localStorage
        if 'browser_storage' not in st.session_state:
            st.session_state.browser_storage = {}
        
        st.subheader("浏览器存储模拟")
        
        col1, col2 = st.columns(2)
        
        with col1:
            # 存储操作
            storage_key = st.text_input("存储键", "user_preference")
            storage_value = st.text_input("存储值", "dark_mode")
            
            if st.button("存储到浏览器"):
                st.session_state.browser_storage[storage_key] = storage_value
                st.success(f"已存储: {storage_key} = {storage_value}")
                st.rerun()
        
        with col2:
            # 读取操作
            read_key = st.selectbox(
                "读取键",
                list(st.session_state.browser_storage.keys()) + [""]
            )
            
            if read_key and st.button("从浏览器读取"):
                value = st.session_state.browser_storage.get(read_key, "键不存在")
                st.info(f"{read_key} = {value}")
            
            if st.button("清空浏览器存储"):
                st.session_state.browser_storage = {}
                st.warning("浏览器存储已清空")
                st.rerun()
        
        # 显示浏览器存储内容
        st.subheader("浏览器存储内容")
        st.json(st.session_state.browser_storage)
        
        # 导出/导入
        export_col1, export_col2 = st.columns(2)
        
        with export_col1:
            if st.button("导出为JSON"):
                json_data = json.dumps(st.session_state.browser_storage, ensure_ascii=False, indent=2)
                st.download_button(
                    label="📥 下载JSON",
                    data=json_data,
                    file_name="browser_storage.json",
                    mime="application/json"
                )
        
        with export_col2:
            uploaded_json = st.file_uploader("导入JSON文件", type=['json'])
            if uploaded_json and st.button("导入JSON"):
                try:
                    imported_data = json.load(uploaded_json)
                    st.session_state.browser_storage.update(imported_data)
                    st.success("JSON数据导入成功")
                    st.rerun()
                except Exception as e:
                    st.error(f"导入失败: {str(e)}")
    
    # 3. 文件存储
    elif persistence_method == "文件存储":
        st.subheader("文件存储持久化")
        
        # 文件存储配置
        file_format = st.selectbox("文件格式", ["JSON", "Pickle", "CSV"])
        
        # 要存储的数据
        st.subheader("要存储的数据")
        
        data_col1, data_col2 = st.columns(2)
        
        with data_col1:
            data_key = st.text_input("数据键", "user_data")
            data_type = st.selectbox("数据类型", ["字典", "列表", "字符串", "数值"])
        
        with data_col2:
            if data_type == "字典":
                dict_key = st.text_input("键", "username")
                dict_value = st.text_input("值", "user123")
                data_value = {dict_key: dict_value}
            elif data_type == "列表":
                list_items = st.text_area("列表项(每行一项)", "item1\nitem2\nitem3")
                data_value = [item.strip() for item in list_items.split('\n') if item.strip()]
            elif data_type == "字符串":
                data_value = st.text_input("字符串值", "Hello, World!")
            else:  # 数值
                data_value = st.number_input("数值", value=42)
        
        # 文件操作
        st.subheader("文件操作")
        
        op_col1, op_col2, op_col3 = st.columns(3)
        
        with op_col1:
            if st.button("💾 保存到文件"):
                try:
                    if file_format == "JSON":
                        file_content = json.dumps({data_key: data_value}, ensure_ascii=False, indent=2)
                        file_name = f"data_{data_key}.json"
                        mime_type = "application/json"
                    elif file_format == "Pickle":
                        file_content = pickle.dumps({data_key: data_value})
                        file_name = f"data_{data_key}.pkl"
                        mime_type = "application/octet-stream"
                    else:  # CSV
                        if isinstance(data_value, dict):
                            import pandas as pd
                            df = pd.DataFrame([data_value])
                            file_content = df.to_csv(index=False)
                        else:
                            file_content = str(data_value)
                        file_name = f"data_{data_key}.csv"
                        mime_type = "text/csv"
                    
                    st.download_button(
                        label=f"📥 下载{file_format}文件",
                        data=file_content,
                        file_name=file_name,
                        mime=mime_type
                    )
                    
                    st.session_state[f"last_saved_{data_key}"] = {
                        'key': data_key,
                        'value': data_value,
                        'format': file_format,
                        'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
                    }
                    st.success("数据已准备下载")
                except Exception as e:
                    st.error(f"保存失败: {str(e)}")
        
        with op_col2:
            uploaded_file = st.file_uploader(
                f"上传{file_format}文件",
                type=[file_format.lower()],
                key=f"upload_{file_format}"
            )
            
            if uploaded_file and st.button("📤 从文件加载"):
                try:
                    if file_format == "JSON":
                        loaded_data = json.load(uploaded_file)
                    elif file_format == "Pickle":
                        loaded_data = pickle.load(uploaded_file)
                    else:  # CSV
                        import pandas as pd
                        loaded_data = pd.read_csv(uploaded_file).to_dict('records')
                    
                    st.session_state[f"loaded_{data_key}"] = {
                        'data': loaded_data,
                        'filename': uploaded_file.name,
                        'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
                    }
                    st.success(f"从 {uploaded_file.name} 加载成功")
                    st.rerun()
                except Exception as e:
                    st.error(f"加载失败: {str(e)}")
        
        with op_col3:
            # 显示最近的操作
            if f"last_saved_{data_key}" in st.session_state:
                st.info("最近保存:")
                st.json(st.session_state[f"last_saved_{data_key}"])
            
            if f"loaded_{data_key}" in st.session_state:
                st.info("最近加载:")
                st.json(st.session_state[f"loaded_{data_key}"])
    
    # 4. 数据库存储模拟
    else:  # 数据库存储
        st.subheader("数据库存储模拟")
        
        st.info("""
        在实际应用中,您可以使用SQLite、PostgreSQL、MySQL等数据库。
        这里我们模拟数据库操作来展示概念。
        """)
        
        # 模拟数据库表
        if 'db_users' not in st.session_state:
            st.session_state.db_users = []
        
        if 'db_products' not in st.session_state:
            st.session_state.db_products = [
                {'id': 1, 'name': '产品A', 'price': 100, 'stock': 50},
                {'id': 2, 'name': '产品B', 'price': 200, 'stock': 30},
                {'id': 3, 'name': '产品C', 'price': 150, 'stock': 20}
            ]
        
        # 数据库操作
        db_tab1, db_tab2, db_tab3 = st.tabs(["用户管理", "产品管理", "数据库操作"])
        
        with db_tab1:
            st.subheader("用户管理")
            
            # 添加用户
            with st.form("add_user_form"):
                st.write("添加新用户")
                user_name = st.text_input("用户名")
                user_email = st.text_input("邮箱")
                user_role = st.selectbox("角色", ["用户", "管理员", "访客"])
                
                if st.form_submit_button("添加用户"):
                    if user_name and user_email:
                        new_user = {
                            'id': len(st.session_state.db_users) + 1,
                            'name': user_name,
                            'email': user_email,
                            'role': user_role,
                            'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
                        }
                        st.session_state.db_users.append(new_user)
                        st.success(f"用户 {user_name} 添加成功")
                        st.rerun()
                    else:
                        st.error("请填写所有必填字段")
            
            # 显示用户列表
            st.subheader("用户列表")
            if st.session_state.db_users:
                users_df = pd.DataFrame(st.session_state.db_users)
                st.dataframe(users_df, use_container_width=True)
            else:
                st.info("暂无用户数据")
        
        with db_tab2:
            st.subheader("产品管理")
            
            # 显示产品列表
            products_df = pd.DataFrame(st.session_state.db_products)
            st.dataframe(products_df, use_container_width=True)
            
            # 产品操作
            col1, col2 = st.columns(2)
            
            with col1:
                # 添加产品
                with st.form("add_product_form"):
                    st.write("添加新产品")
                    prod_name = st.text_input("产品名称")
                    prod_price = st.number_input("价格", min_value=0.0, value=100.0)
                    prod_stock = st.number_input("库存", min_value=0, value=10)
                    
                    if st.form_submit_button("添加产品"):
                        if prod_name:
                            new_product = {
                                'id': len(st.session_state.db_products) + 1,
                                'name': prod_name,
                                'price': prod_price,
                                'stock': prod_stock
                            }
                            st.session_state.db_products.append(new_product)
                            st.success(f"产品 {prod_name} 添加成功")
                            st.rerun()
                        else:
                            st.error("请输入产品名称")
            
            with col2:
                # 更新库存
                product_id = st.number_input("产品ID", min_value=1, value=1)
                new_stock = st.number_input("新库存", min_value=0, value=0)
                
                if st.button("更新库存"):
                    for product in st.session_state.db_products:
                        if product['id'] == product_id:
                            product['stock'] = new_stock
                            st.success(f"产品ID {product_id} 库存更新为 {new_stock}")
                            st.rerun()
                            break
                    else:
                        st.error(f"未找到产品ID {product_id}")
        
        with db_tab3:
            st.subheader("数据库操作")
            
            # 数据库统计
            col1, col2, col3 = st.columns(3)
            
            with col1:
                st.metric("用户数量", len(st.session_state.db_users))
            
            with col2:
                st.metric("产品数量", len(st.session_state.db_products))
            
            with col3:
                total_value = sum(p['price'] * p['stock'] for p in st.session_state.db_products)
                st.metric("库存总价值", f"¥{total_value:,.2f}")
            
            # 数据导出
            st.subheader("数据导出")
            
            export_format = st.selectbox("导出格式", ["JSON", "CSV", "SQL"])
            
            if st.button("导出数据库"):
                export_data = {
                    'users': st.session_state.db_users,
                    'products': st.session_state.db_products,
                    'export_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
                    'data_hash': hashlib.md5(
                        json.dumps(st.session_state.db_users + st.session_state.db_products, sort_keys=True).encode()
                    ).hexdigest()[:8]
                }
                
                if export_format == "JSON":
                    file_content = json.dumps(export_data, ensure_ascii=False, indent=2)
                    file_name = f"database_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
                    mime_type = "application/json"
                elif export_format == "CSV":
                    import pandas as pd
                    # 导出为多个CSV文件
                    users_df = pd.DataFrame(export_data['users'])
                    products_df = pd.DataFrame(export_data['products'])
                    
                    # 创建ZIP文件
                    import zipfile
                    import io
                    
                    zip_buffer = io.BytesIO()
                    with zipfile.ZipFile(zip_buffer, 'w') as zip_file:
                        # 添加用户CSV
                        users_csv = users_df.to_csv(index=False)
                        zip_file.writestr('users.csv', users_csv)
                        
                        # 添加产品CSV
                        products_csv = products_df.to_csv(index=False)
                        zip_file.writestr('products.csv', products_csv)
                        
                        # 添加元数据
                        meta_data = {
                            'export_time': export_data['export_time'],
                            'data_hash': export_data['data_hash']
                        }
                        meta_json = json.dumps(meta_data, indent=2)
                        zip_file.writestr('metadata.json', meta_json)
                    
                    file_content = zip_buffer.getvalue()
                    file_name = f"database_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip"
                    mime_type = "application/zip"
                else:  # SQL
                    # 生成SQL语句
                    sql_statements = []
                    sql_statements.append("-- 数据库导出SQL")
                    sql_statements.append(f"-- 导出时间: {export_data['export_time']}")
                    sql_statements.append(f"-- 数据哈希: {export_data['data_hash']}")
                    sql_statements.append("")
                    
                    # 用户表
                    sql_statements.append("-- 用户表数据")
                    for user in export_data['users']:
                        sql_statements.append(
                            f"INSERT INTO users (id, name, email, role, created_at) "
                            f"VALUES ({user['id']}, '{user['name']}', '{user['email']}', "
                            f"'{user['role']}', '{user['created_at']}');"
                        )
                    
                    sql_statements.append("")
                    
                    # 产品表
                    sql_statements.append("-- 产品表数据")
                    for product in export_data['products']:
                        sql_statements.append(
                            f"INSERT INTO products (id, name, price, stock) "
                            f"VALUES ({product['id']}, '{product['name']}', "
                            f"{product['price']}, {product['stock']});"
                        )
                    
                    file_content = "\n".join(sql_statements)
                    file_name = f"database_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.sql"
                    mime_type = "application/sql"
                
                st.download_button(
                    label=f"📥 下载{export_format}文件",
                    data=file_content,
                    file_name=file_name,
                    mime=mime_type
                )
            
            # 数据库重置
            st.subheader("危险操作")
            
            if st.button("🔄 重置数据库", type="secondary"):
                st.session_state.db_users = []
                st.session_state.db_products = [
                    {'id': 1, 'name': '产品A', 'price': 100, 'stock': 50},
                    {'id': 2, 'name': '产品B', 'price': 200, 'stock': 30},
                    {'id': 3, 'name': '产品C', 'price': 150, 'stock': 20}
                ]
                st.warning("数据库已重置为初始状态")
                st.rerun()

# 导入pandas用于数据库标签页
import pandas as pd

# 页脚
st.divider()
st.caption(f"""
会话状态演示应用 | 最后更新: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} |
会话ID哈希: {hashlib.md5(str(id(st.session_state)).encode()).hexdigest()[:8]}
""")

5. 总结与最佳实践

5.1 Streamlit的核心优势总结

让我们通过一个对比表格来总结Streamlit的优势:

5.2 Streamlit最佳实践

基于我们的示例Demo,以下是Streamlit开发的最佳实践:

  1. 代码组织最佳实践
python 复制代码
# ✅ 推荐结构
import streamlit as st
import other_libraries

# 1. 页面配置
st.set_page_config(...)

# 2. 初始化状态
if 'key' not in st.session_state:
    st.session_state.key = value

# 3. 侧边栏配置
with st.sidebar:
    # 控制组件

# 4. 主内容区
st.title(...)

# 5. 使用缓存优化性能
@st.cache_data
def load_data():
    return data

# 6. 错误处理
try:
    # 业务逻辑
except Exception as e:
    st.error(f"错误: {str(e)}")
  1. 性能优化建议

    • 使用@st.cache_data缓存数据加载

    • 使用@st.cache_resource缓存资源

    • 避免在回调中执行重计算

    • 使用会话状态避免重复初始化

  2. 用户体验优化

    • 使用进度条st.progress()显示长时间操作

    • 使用st.spinner()提供反馈

    • 合理的错误提示和信息展示

    • 响应式布局设计

5.3 实际应用场景

通过前面的Demo,我们已经展示了Streamlit在多个场景的应用:

  1. 数据探索与可视化 ​ - demo_data_explorer.py

  2. 机器学习模型部署​ - 将在后续博客中详细展示

  3. 企业内部工具​ - 快速构建管理界面

  4. 报告生成系统​ - 自动化数据报告

  5. 原型验证​ - 快速验证产品想法

5.4 学习路径建议

6. 常见问题与解决方案

6.1 性能问题

问题: 应用响应缓慢

解决方案:

  1. 使用缓存装饰器

  2. 避免在回调中执行重计算

  3. 使用会话状态保存中间结果

  4. 优化数据加载和预处理

6.2 状态管理问题

问题: 状态丢失或混乱

解决方案:

  1. 使用唯一的键名

  2. 初始化状态时使用默认值

  3. 使用回调函数管理状态更新

  4. 实现状态验证逻辑

6.3 布局问题

问题: 界面在不同设备上显示不一致

解决方案:

  1. 使用响应式布局(layout="wide"

  2. 使用列和容器组织内容

  3. 测试不同屏幕尺寸

  4. 使用主题保持一致性

7. 资源推荐

7.1 官方资源

  • 📚 官方文档

  • 🎮 API参考

  • 💬 社区论坛

  • 📦 组件库

7.2 学习资源

  • 📹 官方教程视频

  • 📖 示例应用库

  • 🏆 最佳实践指南

7.3 社区资源

  • 🌟 Awesome Streamlit

  • 🚀 Streamlit Components

  • 📊 Streamlit for Data Science

8. 下一篇预告

在下一篇博客中,我们将深入探讨:

  1. Streamlit高级功能深度解析

    • 多页面应用架构

    • 自定义主题和样式

    • 高级图表集成

    • 实时数据处理

  2. 更多完整Demo工程

    • 实时数据监控面板

    • 机器学习模型部署平台

    • 团队协作工具

    • 自动化报告系统

  3. 企业级应用开发

    • 项目架构设计

    • 代码组织规范

    • 测试和部署

    • 性能优化策略

总结

本篇博客通过多个完整的Demo工程,从基础到进阶,全面介绍了Streamlit的核心概念和应用场景。我们重点讲解了:

  1. Streamlit的设计哲学​ - 快速原型开发的革命性工具

  2. 核心组件深度解析​ - 从基础组件到高级布局

  3. 状态管理机制​ - 会话状态的完整解决方案

  4. 实际应用开发​ - 完整的数据探索器实现

  5. 最佳实践​ - 性能优化和代码组织

每个Demo都是独立的、完整的Python文件,可以直接运行和学习。通过Mermaid图表,我们清晰地展示了代码结构和数据流程。

Streamlit的真正价值在于它让数据科学家和开发者能够专注于业务逻辑,而不是Web开发的复杂性。通过本篇博客的学习,您应该已经能够:

  1. ✅ 快速搭建Streamlit应用

  2. ✅ 理解并应用状态管理

  3. ✅ 创建交互式数据应用

  4. ✅ 优化应用性能和用户体验

相关推荐
李昊哲小课2 小时前
Python办公自动化教程 - openpyxl让Excel处理变得轻松
python·信息可视化·excel
源码之屋2 小时前
计算机毕业设计:Python出行数据智能分析与预测平台 Django框架 可视化 数据分析 PyEcharts 交通 深度学习(建议收藏)✅
人工智能·python·深度学习·数据分析·django·汽车·课程设计
复园电子3 小时前
KVM与Hyper-V虚拟化环境:彻底解决USB外设映射掉线的底层架构优化
开发语言·架构·php
2301_803554523 小时前
三大编程语言(Python/Go/C++)项目启动全解析
c++·python·golang
给自己做减法3 小时前
AI编程相关概念
人工智能·python·ai编程
郝学胜-神的一滴3 小时前
PyTorch自动微分核心解析:从原理到实战实现权重更新
人工智能·pytorch·python·深度学习·算法·机器学习
小龙报3 小时前
【Coze-AI智能体平台】Coze OpenAPI 开发手册:鉴权、接口调用与 SDK 实践
javascript·人工智能·python·深度学习·microsoft·文心一言·开源软件
databook3 小时前
理论都会,实战就废?7个分析模板,帮你打通任督二脉
python·数据挖掘·数据分析
Kel4 小时前
从Prompt到Response:大模型推理端到端核心链路深度拆解
人工智能·算法·架构