Python技术迷

将 Pandas 换为交互式表格的 Python 库

你是不是也有过这种感觉:本地写个脚本,用 Pandas 把数据一读一算,自己看着很爽;结果产品、运营一过来,“能不能给我一个能点一点、筛一筛的界面啊?” 然后你只好一顿 print(df.head()) 截图发给别人,来回问一句改一次 SQL,非常崩溃。

我这两年基本把“看数据”这件事,慢慢从“Pandas 打印表格”换成“交互式表格”了,其实不复杂,就是把原来的 DataFrame 换个展示方式而已。

下面就按“聊天”的节奏,带你把这个事情理一理,顺手上几段可以直接抄走用的 Python 代码。

Pandas 真心好用,这个不用夸了,随便写几行:

import pandas as pd

df = pd.read_csv("orders.csv")
print(df.head())
print(df.describe())

控制台 / Notebook 里啪一下就出来一大坨表格,看统计也方便。

但问题也很明显:

  • 看的人只能“看”,不能点表头排序、不能在某列上直接过滤
  • 想改一两行数据试试效果,要么改源文件,要么写代码改,再重新跑
  • 想给不写代码的人用,只能截图、导出 Excel、再发来发去

所以很多时候,你脑子里真正想要的是:有个表格界面,能点、能筛、能编辑,底层还是 DataFrame 在跑就行。

于是就轮到各种“交互式表格库”登场了。

我习惯按使用场景来选库,大概分三种:Notebook 里玩、写 Web 小应用、写桌面/内部工具。每类挑一两个说,不搞库大杂烩。

1)Jupyter 里直接变成可点可改的表格:qgrid / mitosheet 这一挂

如果你平时就是在 Jupyter / JupyterLab 里写代码,想要的是“DataFrame 还能点一点”,最顺滑的是这种基于 widget 的库。

举个 qgrid 的简单例子(它本质上还是操作 Pandas 的 df):

import pandas as pd
import qgrid

df = pd.read_csv("orders.csv")

# 创建一个可交互的表格组件
grid = qgrid.show_grid(
    df,
    show_toolbar=True,      # 上面有一排筛选、导出的小工具
    editable=True# 允许在表格里直接改值
)

grid

在 Notebook 里跑完这段,grid 就是一张能点击表头排序、能像 Excel 一样改数据、还能多条件过滤的表格。 你改完之后,还可以把改过的数据拿回来:

df_edited = grid.get_changed_df()

这样你原来所有基于 df 的分析、画图代码,几乎不用改,只要把数据源换成 df_edited 就行。

如果你更偏爱“Excel 一样的感觉”,可以试试类似 mitosheet 这样的东西,直接在 Notebook 里拉出一个类似表格编辑器的界面,背后帮你生成 Pandas 代码。这个就比较适合数据分析同学,写代码少一点,效果也不错。

2)给别人用的 Web 小工具:Streamlit 这一类

当你想把脚本变成“同事也能点”的小页面,Streamlit 这种库就很香。它的 st.dataframe / st.data_editor 天然就是交互式表格。

先看最小可用版本:

# streamlit_app.py
import streamlit as st
import pandas as pd

df = pd.read_csv("orders.csv")

st.title("订单数据小看板")

# 可交互表格:支持排序、拖动列宽、局部滚动之类
st.dataframe(df)

# 新版本可以用 data_editor,表格里直接编辑
edited_df = st.data_editor(df, num_rows="dynamic")

st.write("你刚刚编辑后的前 5 行:")
st.dataframe(edited_df.head())

然后命令行跑:

streamlit run streamlit_app.py

浏览器里就能看到一个带表格的小页面,别人只要能访问你这台机器/服务器,就能上来点点看看。

稍微认真一点,你可以加一些筛选条件,让“交互式表格”真正参与业务逻辑,而不只是展示:

import streamlit as st
import pandas as pd

df = pd.read_csv("orders.csv")

st.title("订单分析小工具")

# 左侧筛选栏
with st.sidebar:
    st.header("筛选条件")
    city = st.multiselect(
"城市",
        options=sorted(df["city"].dropna().unique()),
        default=[]
    )
    min_amount = st.number_input("最小金额", value=0.0)
    max_amount = st.number_input("最大金额", value=float(df["amount"].max()))

# 根据筛选条件过滤 DataFrame
filtered = df.copy()

if city:
    filtered = filtered[filtered["city"].isin(city)]

filtered = filtered[
    (filtered["amount"] >= min_amount) &
    (filtered["amount"] <= max_amount)
]

st.subheader("筛选后的结果")
st.dataframe(filtered)

st.subheader("简单统计")
st.write(filtered.describe())

这一套下来,原来“我发你一份 Excel,你自己筛一筛”的活,就变成“一句链接,你自己点一点击”的体验了,而且所有逻辑都还是你熟悉的 Pandas。

3)对交互要求更复杂:Dash / Bokeh 的 DataTable

有时候产品同学想要的东西比较狠:分页、冻结列、大量列宽调节、复杂条件格式,那就得上稍微重型一点的东西,比如 Dash 的 DataTable。

简化版的 Dash 示例:

from dash import Dash, dash_table, html
import pandas as pd

df = pd.read_csv("orders.csv")

app = Dash(__name__)

app.layout = html.Div([
    html.H1("订单数据交互表格"),
    dash_table.DataTable(
        id="table",
        columns=[{"name": c, "id": c} for c in df.columns],
        data=df.to_dict("records"),
        page_size=20,           # 分页
        filter_action="native", # 表头条件过滤
        sort_action="native",   # 排序
        editable=True# 允许编辑
    )
])

if __name__ == "__main__":
    app.run(debug=True)

这个写法跟普通 Web 开发更像一点,适合你本来就有个 Dash / Flask / Django 项目,要在里头塞一个“富交互表格组件”的场景。

Bokeh 里也有 DataTable,写法类似,就不一一展开了。

从“print(df.head())”到“交互式小工具”:一个完整迁移过程

说归说,很多人卡在“我现在有一个几百行的 Pandas 脚本,怎么不太痛苦地改成交互式的?”

给你看一个真实一点的改造思路,场景假设是这样:

原来你有个 report.py,每天跑一下,把当天订单的数据算一算,输出一些统计。

原始版大概长这样:

# report.py
import pandas as pd

df = pd.read_sql("SELECT * FROM orders WHERE created_at >= CURDATE()", conn)

print("总订单数:", len(df))
print("各城市总金额:")
print(df.groupby("city")["amount"].sum().sort_values(ascending=False))

print("大额订单(> 1000):")
print(df[df["amount"] > 1000].head())

你自己看没问题,但是别人根本用不上。

1. 把入口从“脚本执行”变成“Web 页面”

直接套一层 Streamlit 外壳,把 print 换成 st.write / st.dataframe:

# report_app.py
import streamlit as st
import pandas as pd
from sqlalchemy import create_engine

engine = create_engine("mysql+pymysql://user:pwd@host/db")

st.title("每日订单报告")

@st.cache_data(ttl=600)
defload_data():
    sql = "SELECT * FROM orders WHERE created_at >= CURDATE()"
return pd.read_sql(sql, engine)

df = load_data()

st.write("总订单数:", len(df))

city_amount = df.groupby("city")["amount"].sum().sort_values(ascending=False)
st.write("各城市总金额:")
st.dataframe(city_amount.reset_index())

big_orders = df[df["amount"] > 1000]
st.write("大额订单(> 1000):")
st.dataframe(big_orders)

这里 @st.cache_data 是为了避免每次刷新都查数据库,用缓存顶一会儿。

2. 再加一点交互:让别人自己调阈值、按时间过滤

你稍微再动一动,把“> 1000”这种硬编码丢到侧边栏去:

import streamlit as st
import pandas as pd
from sqlalchemy import create_engine

engine = create_engine("mysql+pymysql://user:pwd@host/db")

st.title("每日订单报告")

with st.sidebar:
    st.header("筛选条件")
    min_amount = st.number_input("大额订单阈值", value=1000.0, step=100.0)
    days = st.slider("最近几天", min_value=1, max_value=30, value=1)

@st.cache_data(ttl=600)
defload_data(days: int):
    sql = f"""
        SELECT * FROM orders
        WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL {days} DAY)
    """

return pd.read_sql(sql, engine)

df = load_data(days)

st.write("总订单数:", len(df))

city_amount = df.groupby("city")["amount"].sum().sort_values(ascending=False)
st.subheader("各城市总金额")
st.dataframe(city_amount.reset_index())

big_orders = df[df["amount"] > min_amount]
st.subheader(f"大额订单(> {min_amount})")
st.dataframe(big_orders)

你看,整套逻辑还是用 Pandas 在算,只不过“表格怎么给人看”从 print 变成了交互式组件,加几个控件就有了一个能给业务方玩半天的小工具。