5张图看懂仓库销售:日趋势、畅销榜、物流偏好、省份排名、促销效果(附Python代码)
《出版社物流WMS智能调度实战(四)》:从表到图 | WMS销售数据日汇总表可视化实战
📖 《出版社物流WMS智能调度实战》系列文章
环境准备篇:Windows/Linux双环境搭建指南 | 第一篇:架构升级 | 第二篇:开发排坑 | 第三篇:运维监控 | 第四篇:本文(销售数据可视化) | 第五篇:预测结果可视化
📌 前置阅读:本文是系列第四篇,建议先阅读环境准备篇完成服务器搭建,以及前三篇了解架构、开发和运维,再进入本篇的可视化实战。
摘要
销售数据日汇总表 WMS_SALES_DAILY_AGG 记录了出版社仓库每天的真实销售数据。如何从这些数字中快速看清业务全貌?本文用五张典型图表展示核心运营指标,所有图表均来自真实数据(已脱敏)。非技术人员可直接阅读图表及解读;技术人员可在附录中找到完整的 Python 代码,一键生成交互式 HTML 报表,并支持钉钉/邮件自动推送。阅读本文约 15 分钟,代码可直接运行。
一、销售日汇总表能回答哪些问题?
| 业务问题 | 对应图表 | 用处 |
|---|---|---|
| 最近一个月销量走势如何? | 日销售趋势折线图 | 判断淡旺季、促销效果 |
| 哪些书卖得最好? | 畅销图书 Top 20 | 指导补货、调整库存位置 |
| 大多数订单用什么物流? | 物流方式订单量 | 优化物流合同、降低成本 |
| 哪些省份订单多? | 省份销量热力图 | 考虑区域分仓 |
| 促销活动有效吗? | 折扣率 vs 销量散点图 | 评估促销ROI |
下面我们直接看图表(数据为脱敏示例,实际运行时替换为真实数据)。
二、图表展示与解读
2.1 日销售趋势(折线图)

📌 解读:
- 横轴为日期,纵轴为每日销售托数(整托数量)和件数(含拆零)。
- 从图中可以清晰看出每周上半周有一个销售高峰(可能是季节性),下半周之后回落。
- 仓库经理(物流调度)可据此提前安排加班或调整出库策略(下半周做好下周的补货移位等工作)。
2.2 畅销图书 Top 20(柱状图)

📌 解读:
- 展示销量最高的 20 个图书编码(实际报表中可关联商品表显示书名)。
- 头部图书占据了约 35% 的销量,应优先将这些图书放在快速区,确保整托出库。
- 采购部门(业务部及物流调度)可关注这些图书的库存,及时沟通采购或图书调拨等补充库存工作,避免缺货。
2.3 物流方式订单量

📌 解读:
- 汽运是主要物流方式。
- 如果快递占比持续上升,可以考虑与快递公司签订更优惠的长期合同。
2.4 销量 Top 10 省份

📌 解读:
- 北京、天津、广东是销量前三的省份。
- 如果条件允许,可以考虑在这些省份设立区域仓,缩短配送时间。
2.5 折扣率 vs 销量

📌 解读:
- 每个点代表一天,横轴为当日平均折扣率(0=原价,0.2=8折),纵轴为日销量(件)。蓝色虚线为线性趋势线。
- 从图中可以看出,折扣率在 0.5-0.6(即 4-5 折)时销量明显提升,但折扣超过 0.7(3 折以下)后销量并未成比例增长,说明折让已经过度。
- 业务营销团队可根据此图优化折扣力度。
三、自动化与推送
以上图表如何每天自动生成并推送给团队?技术流程如下:
- 数据读取:从
WMS_SALES_DAILY_AGG表读取最近 30 天数据。 - 绘图:使用 Python 的
matplotlib(静态图)和plotly(交互式图)生成图表。 - 生成 HTML 报告:将所有图表整合到一个 HTML 文件,支持手机/电脑查看。
- 定时执行:通过 crontab 每天早晨 8:30 自动运行脚本。
- 推送:将生成的 HTML 报告上传至公司内部静态服务器,并通过钉钉机器人发送链接;或直接发送邮件。
详细代码见附录。
四、附录:Python 代码(供技术人员使用)
4.1 环境依赖
pip install pandas matplotlib seaborn plotly jinja2 requests
4.2 完整脚本 generate_sales_report.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px
from datetime import datetime, timedelta
from db_utils import target_db # 复用系列中的数据库模块
import base64
from io import BytesIO
from jinja2 import Template
import requests
import sys
import os
# ==================== 中文字体设置 ====================
if sys.platform.startswith('win'):
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei']
plt.rcParams['axes.unicode_minus'] = False
else:
plt.rcParams['font.sans-serif'] = ['WenQuanYi Zen Hei', 'Noto Sans CJK SC']
plt.rcParams['axes.unicode_minus'] = False
sns.set_style("whitegrid")
# ==================== 数据加载 ====================
def load_sales_data(start_date, end_date):
sql = f"""
SELECT sale_date, item_id, total_tuo, total_qty, total_amount,
order_count, customer_count, avg_discount_rate,
main_delivery_type, main_province, publisher_code
FROM WMS_SALES_DAILY_AGG
WHERE sale_date BETWEEN :start AND :end
"""
return target_db.query(sql, {'start': start_date, 'end': end_date})
# ==================== 静态图(matplotlib)转 base64 ====================
def fig_to_base64(fig):
buf = BytesIO()
fig.savefig(buf, format='png', bbox_inches='tight')
buf.seek(0)
return base64.b64encode(buf.read()).decode('utf-8')
# ==================== 图表生成 ====================
def plot_daily_trend(df):
daily = df.groupby('sale_date').agg({'total_tuo': 'sum', 'total_qty': 'sum'}).reset_index()
fig, ax = plt.subplots(figsize=(12, 5))
ax.plot(daily['sale_date'], daily['total_tuo'], marker='o', label='托数')
ax.plot(daily['sale_date'], daily['total_qty'], marker='s', label='件数')
ax.set_xlabel('日期'); ax.set_ylabel('销量'); ax.set_title('日销售趋势')
ax.legend(); plt.xticks(rotation=45); plt.tight_layout()
return fig
def plot_top_books(df, top_n=20):
book_sales = df.groupby('item_id')['total_tuo'].sum().reset_index()
book_sales = book_sales.sort_values('total_tuo', ascending=False).head(top_n)
fig = px.bar(book_sales, x='item_id', y='total_tuo', title=f'畅销图书 Top {top_n}(托数)')
return fig
def plot_delivery_method(df):
delivery = df.groupby('main_delivery_type')['order_count'].sum().reset_index()
fig = px.bar(delivery, x='main_delivery_type', y='order_count', title='各物流方式订单量')
return fig
def plot_province_heatmap(df):
province = df.groupby('main_province')['total_qty'].sum().reset_index()
province = province.sort_values('total_qty', ascending=False).head(10)
fig = px.bar(province, x='main_province', y='total_qty', title='销量 Top 10 省份')
return fig
def plot_discount_vs_sales(df):
daily = df.groupby('sale_date').agg({'avg_discount_rate': 'mean', 'total_qty': 'sum'}).reset_index()
fig = px.scatter(daily, x='avg_discount_rate', y='total_qty', text='sale_date',
title='折扣率与销量关系', labels={'avg_discount_rate': '平均折扣率', 'total_qty': '日销量(件)'})
return fig
# ==================== 生成 HTML 报告 ====================
def generate_report(start_date=None, end_date=None):
if start_date is None:
end_date = datetime.now().date() - timedelta(days=1)
start_date = end_date - timedelta(days=29)
df = load_sales_data(start_date, end_date)
if df.empty:
print("无数据")
return
# 生成图表
fig1 = plot_daily_trend(df)
fig2 = plot_top_books(df)
fig4 = plot_delivery_method(df)
fig5 = plot_province_heatmap(df)
fig6 = plot_discount_vs_sales(df)
daily_trend_img = fig_to_base64(fig1)
top_books_html = fig2.to_html(full_html=False)
delivery_html = fig4.to_html(full_html=False)
province_html = fig5.to_html(full_html=False)
discount_html = fig6.to_html(full_html=False)
html_template = """
<html>
<head><meta charset="UTF-8"><title>销售日汇总报表</title></head>
<body>
<h1>销售日汇总报表</h1>
<p>统计区间: {{ start_date }} 至 {{ end_date }}</p>
<h2>1. 日销售趋势</h2>
<img src="data:image/png;base64,{{ daily_trend_img }}" style="width:100%"/>
<h2>2. 畅销图书 Top 20</h2>
{{ top_books_html }}
<h2>3. 物流方式订单量</h2>
{{ delivery_html }}
<h2>4. 销量 Top 10 省份</h2>
{{ province_html }}
<h2>5. 折扣率 vs 销量</h2>
{{ discount_html }}
</body>
</html>
"""
template = Template(html_template)
html_content = template.render(
start_date=start_date, end_date=end_date,
daily_trend_img=daily_trend_img,
top_books_html=top_books_html,
delivery_html=delivery_html,
province_html=province_html,
discount_html=discount_html
)
filename = f"sales_report_{datetime.now().strftime('%Y%m%d')}.html"
with open(filename, "w", encoding='utf-8') as f:
f.write(html_content)
print(f"报表已生成: {filename}")
return filename
# ==================== 推送钉钉 ====================
def send_dingtalk(file_url, webhook):
data = {
"msgtype": "text",
"text": {"content": f"销售日报已生成,点击查看:{file_url}"}
}
requests.post(webhook, json=data)
if __name__ == "__main__":
report_file = generate_report()
# 若需要推送到钉钉,取消下一行注释并配置 webhook 和文件访问 URL
# send_dingtalk("https://your-server.com/" + report_file, "your_webhook")
# 如发送邮件等功能,增加发送邮件函数后在此调用即可。
4.3 定时任务(crontab)
# 每天 8:30 生成昨日销售报表
30 8 * * * cd /opt/wms_ml && source venv/bin/activate && python generate_sales_report.py >> logs/sales_report.log 2>&1
⚠️ 请根据实际安装路径修改 /opt/wms_ml 和虚拟环境名称。
五、常见问题与排坑
| 问题 | 解决方案 |
|---|---|
matplotlib 中文字体显示方框 |
设置 plt.rcParams['font.sans-serif'] = ['Microsoft YaHei'](Windows)或 ['WenQuanYi Zen Hei'](Linux) |
DPI-1047 找不到 Oracle 客户端 |
注释掉厚模式代码,改用 Thin 模式;或正确安装 Oracle Instant Client |
| 生成的 HTML 中部分图表为空白 | 检查 plotly 是否正常安装,尝试在浏览器中打开 F12 查看控制台错误 |
| 查询结果为空 | 检查销售日汇总表 WMS_SALES_DAILY_AGG 是否有数据,以及日期范围是否正确 |
to_html(full_html=False) 样式缺失 |
可尝试使用 fig.to_html(include_plotlyjs='cdn') 并保留完整 HTML 头 |
| crontab 执行脚本无日志 | 在 crontab 中使用绝对路径,并确保 logs 目录有写权限 |
六、总结
本文用五张图表展示了销售数据日汇总表的核心价值,非技术人员可直接看图决策;技术人员可按附录代码实现自动化报表生成与推送。让数据从“躺在数据库”变成“摆在桌面”,真正驱动仓库精细化运营。
📖 系列回顾
- 环境准备篇:Windows/Linux双环境搭建指南
- 第一篇:架构升级与机器学习落地
- 第二篇:从“卡死”到“跑通”——开发排坑记
- 第三篇:从“卡死”到“跑稳”——运维监控与自动回滚
- 第四篇:本文(销售数据可视化)
- 第五篇:预测结果可视化与模型效果监控
- 第六篇:自助式 BI 集成(即将发布)
下期预告:第五篇《预测结果可视化与模型效果监控》已发布,围绕 WMS_ML_FORECAST_TUO 表制作预测 vs 实际对比图、误差分析看板。第六篇《自助式 BI 集成》将介绍如何将销售日汇总表和预测结果表接入 Power BI / FineReport,让业务人员自己拖拽分析,敬请期待。
📌 资料包领取:本文配套完整代码、建表SQL、调度脚本已整理,关注后私信“销售可视化”即可获取。
互动:你的仓库最关心哪些销售指标?欢迎在评论区分享,我会抽取典型问题在下期解答。
更多推荐




所有评论(0)