Featured image of post 用 DuckDB 搭建自动化电商数据监控看板

用 DuckDB 搭建自动化电商数据监控看板

从零搭建一套电商自动化数据监控看板:CSV 直读、环比分析、异常检测 Z-Score、HTML 看板生成,全流程不到 100 行代码,可变现为数据服务产品。

用 DuckDB 搭建自动化电商数据监控看板

一个数据产品思路:帮电商品牌方搭建月度销售监控看板,每个客户收费 3000-5000 元/月,全链路不到 100 行代码。


一、从需求到产品的完整思路

很多数据分析师接到过这样的需求:

“帮我盯一下这几个店铺的销售数据,每天自动出报表,有异常要第一时间告诉我。”

传统的实现方式是什么?Python 脚本调 API → 存 MySQL → Jupyter Notebook 分析 → 导出 Excel → 邮件发送。一套流程跑下来,少说三小时,维护成本还极高。

用 DuckDB,整个流程可以压缩到不到 100 行代码。而且这套方案我用来给三个品牌方做月度数据服务,每个客户收费 3000-5000 元/月。

核心思路:DuckDB 可以直接读取 CSV 文件(read_csv_auto),无需导入数据库;用窗口函数 LAG() 算环比;用移动平均 ± 标准差做异常检测;最后导出 HTML 看板让客户自己看。

二、数据准备

假设你的数据源如下:

  • orders.csv — 订单表(order_id, order_date, product_id, customer_id, amount)
  • products.csv — 商品表(product_id, category, shop_name)
  • customers.csv — 客户表(customer_id, city, member_level)

你可以用任意电商平台导出的 CSV,结构匹配即可。DuckDB 的 read_csv_auto() 会自动推断列类型,无需手动定义 schema。

import duckdb
import pandas as pd

con = duckdb.connect(':memory:')

# 直接读取 CSV,无需导入数据库
orders = con.execute("SELECT * FROM read_csv_auto('orders.csv')").fetchdf()
products = con.execute("SELECT * FROM read_csv_auto('products.csv')").fetchdf()
customers = con.execute("SELECT * FROM read_csv_auto('customers.csv')").fetchdf()

技巧:如果数据量较大(超过内存),可以用 duckdb.connect('cache.duckdb') 打开持久化数据库,DuckDB 会自动建立列存格式并做内存映射。

三、核心分析:多维度聚合 + 环比计算

接下来是核心环节——用一条 SQL 同时完成多维度聚合和环比计算:

daily_dashboard = con.execute("""
    WITH order_stats AS (
        SELECT 
            DATE(o.order_date) AS sale_date,
            p.category,
            p.shop_name,
            COUNT(*) AS order_count,
            SUM(o.amount) AS total_revenue,
            AVG(o.amount) AS avg_order_value,
            COUNT(DISTINCT o.customer_id) AS unique_customers
        FROM orders o
        JOIN products p ON o.product_id = p.product_id
        GROUP BY DATE(o.order_date), p.category, p.shop_name
    ),
    day_over_day AS (
        SELECT *,
            LAG(total_revenue) OVER (
                PARTITION BY shop_name ORDER BY sale_date
            ) AS prev_day_revenue,
            LAG(order_count) OVER (
                PARTITION BY shop_name ORDER BY sale_date
            ) AS prev_day_orders
        FROM order_stats
    )
    SELECT 
        sale_date,
        shop_name,
        category,
        order_count,
        ROUND(total_revenue, 2) AS total_revenue,
        ROUND(avg_order_value, 2) AS avg_order_value,
        unique_customers,
        CASE 
            WHEN prev_day_revenue IS NOT NULL THEN 
                ROUND((total_revenue - prev_day_revenue) / prev_day_revenue * 100, 2)
            ELSE NULL 
        END AS revenue_change_pct,
        CASE 
            WHEN prev_day_orders IS NOT NULL AND prev_day_orders > 0 THEN
                ROUND((order_count - prev_day_orders) / prev_day_orders * 100, 2)
            ELSE NULL 
        END AS orders_change_pct
    FROM day_over_day
    WHERE sale_date >= CURRENT_DATE - INTERVAL '30' DAY
    ORDER BY sale_date DESC, total_revenue DESC
""").fetchdf()

这里有两个关键技巧:

  1. CTE 链式查询 — 先用 order_stats 做聚合,再用 day_over_day 做窗口函数计算,逻辑清晰便于维护。
  2. LAG 窗口函数 — 不用 Python 循环就能算出前一天的数据,进而计算环比变化百分比。

四、异常检测:Z-Score 自动发现数据异动

光看报表不够,还要自动识别异常。下面这段代码用 7 天移动平均 ± 2 倍标准差 的统计方法找出销量骤降的品类:

alert_sql = """
    WITH category_trend AS (
        SELECT 
            category,
            DATE(sale_date) AS dt,
            SUM(total_revenue) AS daily_rev,
            AVG(daily_rev) OVER (
                ORDER BY dt 
                ROWS BETWEEN 6 PRECEDING AND 1 PRECEDING
            ) AS moving_avg_7d,
            STDDEV(daily_rev) OVER (
                ORDER BY dt 
                ROWS BETWEEN 6 PRECEDING AND 1 PRECEDING
            ) AS moving_std_7d
        FROM order_stats
        WHERE sale_date >= CURRENT_DATE - INTERVAL '14' DAY
        GROUP BY category, DATE(sale_date)
    )
    SELECT 
        category,
        dt,
        ROUND(daily_rev, 2) AS today_revenue,
        ROUND(moving_avg_7d, 2) AS seven_day_avg,
        ROUND((daily_rev - moving_avg_7d) / NULLIF(moving_std_7d, 0), 2) AS z_score
    FROM category_trend
    WHERE dt = (SELECT MAX(dt) FROM category_trend)
      AND daily_rev < moving_avg_7d - 2 * moving_std_7d
    ORDER BY z_score ASC
"""

alerts = con.execute(alert_sql).fetchdf()
print(f"⚠️ 发现 {len(alerts)} 个异常品类")
for _, row in alerts.iterrows():
    print(f"  - {row['category']}: 今日 {row['today_revenue']} vs 均值 {row['seven_day_avg']} (Z-Score: {row['z_score']})")

原理说明

  • 用前 7 天的每日营收计算移动平均和标准差
  • 如果今天的数据低于 均值 - 2×标准差,说明出现了显著下降
  • Z-Score 越负,异常程度越高

全部在 SQL 里完成,不需要任何 Python 循环。

五、生成 HTML 看板:让客户自己看

最后一步,把分析结果导出为可直接分享的 HTML 文件:

import html

html_content = f"""
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>电商销售监控看板 - {pd.Timestamp.now().strftime('%Y-%m-%d')}</title>
    <style>
        body {{ font-family: -apple-system, sans-serif; margin: 20px; background: #f5f5f5; }}
        .card {{ background: white; border-radius: 8px; padding: 20px; margin: 10px 0; 
                 box-shadow: 0 1px 3px rgba(0,0,0,0.1); }}
        .metric {{ display: inline-block; margin-right: 30px; }}
        .metric-value {{ font-size: 28px; font-weight: bold; color: #1a73e8; }}
        .metric-label {{ font-size: 12px; color: #999; }}
        .negative {{ color: #e74c3c; }}
        .positive {{ color: #27ae60; }}
    </style>
</head>
<body>
    <h1>📊 电商销售监控看板</h1>
    <p>数据日期:{pd.Timestamp.now().strftime('%Y-%m-%d %H:%M')}</p>
"""

# 插入核心指标
summary = con.execute("""
    SELECT 
        COUNT(*) as total_orders,
        ROUND(SUM(total_revenue), 2) as total_revenue,
        ROUND(AVG(avg_order_value), 2) as avg_order_value
    FROM order_stats
""").fetchone()

html_content += f"""
    <div class="card">
        <h2>核心指标(近 30 天)</h2>
        <div class="metrics">
            <div class="metric"><div class="metric-value">{summary[0]}</div><div class="metric-label">总订单数</div></div>
            <div class="metric"><div class="metric-value">¥{summary[1]:,.0f}</div><div class="metric-label">总营收</div></div>
            <div class="metric"><div class="metric-value">¥{summary[2]:,.0f}</div><div class="metric-label">客单价</div></div>
        </div>
    </div>
"""

# 插入告警模块
if len(alerts) > 0:
    html_content += '<div class="card"><h2 style="color:#e74c3c">⚠️ 异常告警</h2><ul>'
    for _, row in alerts.iterrows():
        html_content += f"<li><b>{row['category']}</b>: 今日 ¥{row['today_revenue']} (Z-Score: {row['z_score']})</li>"
    html_content += '</ul></div>'

html_content += '</body></html>'

with open('dashboard.html', 'w') as f:
    f.write(html_content)

print("✅ 看板已生成: dashboard.html")

生成的 HTML 文件可以直接通过邮件发送、放在内网服务器上,或者用任何静态托管服务发布。

六、与传统方案的对比

维度传统方案(Python + MySQL + Jupyter)DuckDB 方案
数据加载先导入 MySQL,再查询直接读 CSV,零 ETL
环比计算Python 循环 + merge一行 SQL LAG()
异常检测需要导入 statsmodels 等库窗口函数 + 数学运算
代码量200+ 行不到 100 行
运行时间30 秒 ~ 数分钟1~3 秒
部署成本需要 MySQL 服务器一行 pip install duckdb

七、变现建议

这套方案的核心价值在于把数据分析变成可订阅的数据服务产品

  1. 月度监控服务(3000-5000 元/月/客户):为电商品牌方提供自动化的销售看板,每天更新,异常自动告警。
  2. SaaS 化升级:用 FastAPI 封装成 API,前端用 Streamlit 或 Evidence 做多租户看板,每个客户独立数据源。
  3. 行业模板:把这套逻辑包装成通用模板(零售、餐饮、教育),批量卖给同行业商家。
  4. 数据增值服务:在监控基础上增加竞品对比、趋势预测等高级功能,收费翻倍。

关键思路:不要按项目收费,要按订阅收费。DuckDB 的低运维成本让你的数据服务可以覆盖大量小客户,积少成多。


📖 本文的完整可运行代码(含示例数据生成)已发布在 duckdblab.org,包含更详细的部署步骤和更多电商场景案例。

💡 想系统学习 DuckDB 实战?duckdblab.org 上有从入门到进阶的完整教程系列。

📺 Watch video tutorials → DuckDB Lab YouTube

Subscribe for more DuckDB & AI automation tutorials

使用 Hugo 构建
主题 StackJimmy 设计