
简介针对某商场货物销售数据这套Python数据分析项目覆盖了从数据清洗、聚合汇总到可视化分析输出的完整工作流适合用作课程设计、期末大作业或数据分析入门实战素材也适合已有Python基础但缺少真实业务场景的读者提升技能。项目利用pandas对销售日报、进销存等表格进行多表合并、SKU维度汇总与有效库存计算并生成可视化图表完整代码2000余行配套图文分析文档4000余字读者可对照代码逐步理解数据处理逻辑、业务指标含义和结论表达方式。资源包共9个文件以7份Excel数据表为主涵盖商品销售按日汇总、SKU唯一/不唯一、进销存原始表与有效库存等数据形态另含1份docx分析报告和1份py脚本整体约64.65MB压缩包结构清晰便于按需查看。当前已有9901人学习说明这套项目的实战性和参考价值受到认可能提供一份从数据到结论的完整示范。1. 销售分析大作业从原始表到图文报告一套能直接复用的流程拿到一份商场销售分析的课程大作业最怕的不是不会写代码而是数据杂乱到不知道从哪下手。这份资源里既有“商品销售按日汇总报表 01-05月.xlsx”这种标准表也有“SKU 不唯一”的脏数据版本还有“商品进销存原始表 总库存.xlsx”“有效库存.xlsx”这类库存侧文件。对于想用 Python 完成数据清洗、透视聚合、可视化出图的从业者来说这套材料完整覆盖了“原始表 → 分析表 → 图文报告”的典型链路。资源中的销售分析.py 据我经验看是一个 2000 行左右的长脚本配合 4000 字的 Word 图文分析文档适合作为 pandas 数据分析和 Matplotlib 可视化的综合演练。如果你正在准备数据分析岗位的笔试题、课程设计或者需要快速掌握一套“拿到业务 Excel 就能开干”的分析套路下面这个拆解过程可以直接照着跑。2. 数据清洗先把“SKU 不唯一”和“总库存/有效库存”的坑填平2.1 读取 Excel 时最容易翻车的三个细节拿到原始 Excel 文件第一步是确认表头结构、日期格式和空值分布。常见做法是先不急着写业务逻辑用 pandas 把每个 sheet 的 shape、dtypes、缺失值比例打出来看一眼。import pandas as pd sales_raw pd.read_excel(商品销售按日汇总报表 01-05月.xlsx, sheet_name0) print(sales_raw.shape) print(sales_raw.dtypes) print(sales_raw.isnull().sum()) inv_total pd.read_excel(商品进销存原始表 总库存.xlsx, sheet_name0) inv_valid pd.read_excel(商品进销存原始表 有效库存.xlsx, sheet_name0) print(总库存表列名:, inv_total.columns.tolist()) print(有效库存表列名:, inv_valid.columns.tolist())这段代码里sheet_name0表示读第一个工作表dtypes用来检查日期列是否被识别成 object 类型这在 Excel 中非常常见——明明是日期读进来却变成字符串。isnull().sum()则是快速定位哪些列存在空值比如某些商品的 SKU 在部分行是空着的。注意“SKU 不唯一”这个文件它意味着同一 SKU 可能对应多行不同价格或不同分类的记录不能直接 groupby SKU 求和。2.2 处理 SKU 不唯一不要盲目去重先判断重复粒度“SKU 不唯一”在真实业务里通常有两种情况一是同一个 SKU 在不同日期都有销售记录这属于正常的时间序列重复二是同一个 SKU 在同一日期出现多行但价格或数量不一致这才是真正的脏数据。处理逻辑要先按日期 SKU 分组检查同组内是否出现数值字段冲突。dup_check sales_raw.groupby([销售日期, SKU]).agg( 行数(SKU, size), 价格种类(销售单价, nunique), 数量总和(销售数量, sum) ).reset_index() conflict dup_check[dup_check[价格种类] 1] print(存在价格冲突的日期-SKU组合数:, len(conflict))这段代码的关键是nunique统计唯一价格数量。如果价格种类大于 1说明同一 SKU 在同一天卖出了不同单价这可能是促销、拆单或者录入错误。对于这类冲突数据我一般会保留记录数最多的那条价格作为该日基准价或者按数量加权平均计算当日真实单价。如果只是行数多但价格一致那直接 groupby 求和即可不用特殊处理。2.3 总库存与有效库存字段对齐后再做进销存勾稽总库存代表仓库账面数量有效库存代表可售数量。两者之差通常是残次品、冻结库存或已下单未出库的部分。分析时要先把两张表的 SKU、品类字段对齐然后计算“不可售比例”观察哪些商品滞销严重。merged_inv pd.merge( inv_total[[SKU, 商品名称, 总库存量]], inv_valid[[SKU, 有效库存量]], onSKU, howouter, indicatorTrue ) merged_inv[不可售数量] merged_inv[总库存量].fillna(0) - merged_inv[有效库存量].fillna(0) merged_inv[不可售比例] merged_inv[不可售数量] / merged_inv[总库存量].replace(0, np.nan) bad_inv merged_inv.sort_values(不可售数量, ascendingFalse).head(20) print(bad_inv[[SKU, 商品名称, 总库存量, 有效库存量, 不可售比例]])howouter会保留只在某一张表出现的 SKU便于发现库存记录缺失问题indicatorTrue会生成_merge列帮你快速甄别哪些 SKU 只在总库存表里、哪些只在有效库存表里。replace(0, np.nan)避免分母为零产生 inf。这套操作做完进销存两侧的数据就具备了做月末结存核对的条件。2.4 日期字段标准化把“01-05月”这类文本变成可排序日期原文件名带“01-05月”实际 Excel 里的日期列有可能是 “2024-01-01” 标准格式也可能是 “1月1日” 这种文本。统一处理方式是让 pandas 自己推断格式推断失败再手动指定。sales_raw[销售日期] pd.to_datetime(sales_raw[销售日期], errorscoerce) invalid_dates sales_raw[销售日期].isnull().sum() print(无法解析的日期行数:, invalid_dates) sales_raw[月份] sales_raw[销售日期].dt.to_period(M) monthly_sales sales_raw.groupby(月份).agg( 销售额(销售金额, sum), 订单量(销售数量, sum) ).reset_index() print(monthly_sales)errorscoerce是关键参数解析失败会转为 NaT 而不是报错中断之后可以用isnull()精确统计脏日期行数。dt.to_period(M)比单纯的dt.month更好用因为后者会丢掉年份跨年数据会混在一起。这里得到的monthly_sales就是后续所有月度趋势图的直接数据源。3. 核心分析透视聚合、同环比与 Top 商品排名计算3.1 从明细表到日汇总表groupby 与 pivot_table 怎么选文具题目里同时给了“商品销售按日汇总报表”和“商品销售按日汇总新”两个文件说明原始明细和汇总表并存。实际分析时没必要重复造轮子但需要验证汇总表是否与明细表一致。验证方式是用明细表自己 groupby 一份日汇总然后与现成汇总表做差。daily_self sales_raw.groupby([销售日期, SKU]).agg( 日销量(销售数量, sum), 日销售额(销售金额, sum) ).reset_index() daily_compare daily_self.merge( daily_report[[销售日期, SKU, 日销量, 日销售额]], on[销售日期, SKU], howouter, suffixes(_自己, _现成) ) daily_compare[销量差] daily_compare[日销量_自己] - daily_compare[日销量_现成] print(daily_compare[daily_compare[销量差].abs() 0.01].head(10))groupby适合做按列聚合pivot_table适合做行列交叉透视。如果只是生成日汇总groupby 更快如果要做“日期 × 品类”的矩阵用于热力图pivot_table 更合适。上面这段代码的核心在于suffixes参数——合并后同名字段会自动加上后缀不会互相覆盖。如果“销量差”全为零说明现成汇总可靠后续直接用它出图即可省掉大量计算。3.2 月度环比与同比shift 方法的边界处理销售分析的常规输出之一是月度趋势和环比变化。环比就是本月与上月比公式是 (本月 - 上月) / 上月。用 pandas 的shift(1)可以轻松拿到上一期数值但要小心首月数据没有上月参照计算结果是 NaN。monthly_sales[环比增速] monthly_sales[销售额].pct_change() * 100 monthly_sales[上月销售额] monthly_sales[销售额].shift(1) monthly_sales[环比_手动] ( (monthly_sales[销售额] - monthly_sales[上月销售额]) / monthly_sales[上月销售额] * 100 ) print(monthly_sales[[月份, 销售额, 环比增速, 环比_手动]])pct_change()内部就是(当前值 - 前值) / 前值与手写公式完全等价。之所以要手写一列是为了让初学者看到计算过程避免黑盒调用。需要注意shift(1)对排序敏感执行前务必对月份列做sort_values否则取到的“上月”根本不是逻辑上的上月。如果数据跨年还要先按年份分组再 shift否则 1 月会错误地拿去年 12 月做环比。3.3 Top 商品排名多维度筛选比单纯排序更有业务意义只排销售额 Top 10 太单薄更推荐做三个维度的排名交叉分析销售额 Top、销量 Top、库存积压 Top。把这三份名单合并到一张表就能识别出“卖得好但库存不足”的高风险商品。revenue_top sales_raw.groupby(SKU)[销售金额].sum().nlargest(10).reset_index() revenue_top.columns [SKU, 总销售额] qty_top sales_raw.groupby(SKU)[销售数量].sum().nlargest(10).reset_index() qty_top.columns [SKU, 总销量] inv_summary merged_inv[[SKU, 总库存量, 有效库存量, 不可售比例]] combined revenue_top.merge(qty_top, onSKU, howouter).merge(inv_summary, onSKU, howleft) combined[库存可支撑天数] combined[有效库存量] / (combined[总销量] / 5) print(combined.sort_values(总销售额, ascendingFalse))这里的“库存可支撑天数”用的是 5 个月销售均值作为日销估算是一个粗粒度但实用的指标。nlargest(10)直接在 Series 上取前 10比 sort_values head 更简洁。你可能会问为什么用outer合并 Top 名单因为有的 SKU 销售额高但销量排不进前 10反之亦然outer 可以保留两侧所有候选商品再统一关联库存信息。3.4 Excel 多 sheet 输出把分析结果回写给业务方分析做完总要交付直接给 Python 脚本不现实最稳妥的是输出一个带多个 sheet 的 Excel 工作簿每个 sheet 放一张分析表。pandas 的ExcelWriter可以做到而且可以追加 sheet。with pd.ExcelWriter(销售分析结果.xlsx, engineopenpyxl) as writer: monthly_sales.to_excel(writer, sheet_name月度销售趋势, indexFalse) daily_compare.to_excel(writer, sheet_name日汇总核对, indexFalse) combined.to_excel(writer, sheet_nameTop商品分析, indexFalse) sheet_names writer.sheets.keys() print(已输出 sheets:, list(sheet_names))engineopenpyxl是写入 .xlsx 的标准引擎。用with块管理 writer可以在循环结束后自动保存文件不会因为异常导致文件损坏。对于 2000 行的脚本来说一般会在最后封装一个export_results()函数把所有输出集中到一个目录这样图文分析文档里引用数据时路径是稳定的。4. 可视化出图Matplotlib 与 Pandas 内置绘图的双路组合4.1 中文字体与负号两个必踩的显示坑Matplotlib 默认字体不支持中文画出来的图全是方框。这不是代码逻辑问题是字体配置问题。推荐在脚本开头统一设置字体和负号显示避免每张图单独处理。import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [Microsoft YaHei, SimHei, Arial Unicode MS] plt.rcParams[axes.unicode_minus] False print(字体设置完成当前字体:, plt.rcParams[font.sans-serif][0])Linux 服务器上通常没有微软雅黑需要改成系统已有的中文字体。先执行fc-list :langzh查看可用字体再把字体名填进列表。axes.unicode_minus False是让负号正常显示的关键不设置的话 Y 轴负值会变成方块。建议把这个配置片段单独放到config.py或脚本头部所有图表共用。4.2 折线图月度销售额趋势一眼看清淡旺季月度趋势图用折线图最直观但要注意横坐标密度问题——月份少还好如果按日画40 个点挤在一起就会像热搜词里说的“python画图横坐标太密集”。解决方案是旋转刻度或稀疏显示。fig, ax1 plt.subplots(figsize(12, 6)) ax1.plot( monthly_sales[月份].astype(str), monthly_sales[销售额], markero, linewidth2, color#2E86AB, label月度销售额 ) for i, v in enumerate(monthly_sales[销售额]): ax1.annotate(f{v:,.0f}, (i, v), textcoordsoffset points, xytext(0, 8), hacenter, fontsize9) ax1.set_xlabel(月份) ax1.set_ylabel(销售额元) ax1.set_xticklabels(monthly_sales[月份].astype(str), rotation30) ax1.legend() ax1.grid(axisy, alpha0.4) plt.tight_layout() plt.savefig(月度销售额趋势.png, dpi200)set_xticklabels配合rotation30解决横坐标重叠问题。annotate给每个点标数值方便在 Word 报告里直接引用具体数字。dpi200是图文分析文档出图的最低要求太低了打印会模糊。如果月份数超过 10建议隔一个标注一个避免图上全是数字。4.3 柱状图与条形图品类对比和 Top10 排行的最佳表达销售额对比用柱状图Top10 排名用横向条形图更合适因为品类名称较长时横向条可以完整显示。下面这段代码实现的是“品类 × 月度销售额”的分组柱状图可以直接帮助发现哪个品类拉动整体增长。category_month sales_raw.pivot_table( index月份, columns商品大类, values销售金额, aggfuncsum ).fillna(0) ax category_month.plot(kindbar, figsize(14, 7), colormapviridis) ax.set_xlabel(月份) ax.set_ylabel(销售额元) ax.set_xticklabels(ax.get_xticklabels(), rotation0) ax.legend(title商品大类, bbox_to_anchor(1.01, 1)) plt.tight_layout() plt.savefig(品类月度对比.png, dpi200)pivot_table的行是月份、列是商品大类、值是销售额aggfuncsum指定聚合方式。fillna(0)的意义在于某些品类某月可能没有销售记录不填充的话会变成 NaN 导致柱状图出现空缺。bbox_to_anchor(1.01, 1)把图例放到图外右侧省去柱状图被图例遮挡的问题。4.4 双 Y 轴组合图销售额与环比增速放在同一张图里销售额和环比增速量纲不同销售额可能是几十万环比增速是百分比放同一坐标系会有一个序列被压平。双 Y 轴可以解决这个问题左轴画销售额柱状图右轴画环比增速折线。from matplotlib.ticker import FuncFormatter fig, ax_left plt.subplots(figsize(12, 6)) ax_left.bar(monthly_sales[月份].astype(str), monthly_sales[销售额], color#A5C9CA, label销售额) ax_left.set_ylabel(销售额元) ax_left.set_xlabel(月份) ax_right ax_left.twinx() ax_right.plot(monthly_sales[月份].astype(str), monthly_sales[环比增速], color#E07A5F, markers, linewidth2, label环比增速) ax_right.set_ylabel(环比增速%) lines1, labels1 ax_left.get_legend_handles_labels() lines2, labels2 ax_right.get_legend_handles_labels() ax_left.legend(lines1 lines2, labels1 labels2, locupper left) plt.tight_layout() plt.savefig(销售额与环比组合图.png, dpi200)twinx()创建一个共享 X 轴的副坐标轴主坐标轴画柱状图副坐标轴画折线图。合并图例时先分别取两个轴的 handles 和 labels再拼接传给legend()否则只有左侧柱子的图例。这张组合图在图文分析文档里属于“一张图说明一个观点”的标准示范——既能看规模又能看趋势。5. 图文报告生成与脚本组织从 py 文件到最终交付5.1 用 python-docx 自动写入分析结论4000 字的图文分析文档不一定要手写可以先让 Python 把分析结果表格化再通过 python-docx 插入 Word 文档最后人工润色文字结论。这样数据准确性和排版效率都能保证。from docx import Document from docx.shared import Inches doc Document() doc.add_heading(销售分析报告, level0) doc.add_heading(一、销售概况, level1) p doc.add_paragraph(本报告基于2024年1月至5月销售明细数据) p.add_run(累计销售额为).bold True p.add_run(f{monthly_sales[销售额].sum():,} 元。) doc.add_heading(二、图表展示, level1) doc.add_picture(月度销售额趋势.png, widthInches(6)) doc.add_picture(销售额与环比组合图.png, widthInches(6)) doc.add_heading(三、Top商品与库存分析, level1) table doc.add_table(rows1, colscombined.shape[1]) table.style Light Grid Accent 1 for j, col in enumerate(combined.columns): table.rows[0].cells[j].text str(col) for i, row in combined.head(5).iterrows(): cells table.add_row().cells for j, val in enumerate(row): cells[j].text f{val:.2f} if isinstance(val, float) else str(val) doc.save(销售分析报告.docx) print(Word 报告已生成)doc.add_heading的 level 参数控制标题层级level0 是文章主标题。doc.add_picture插入图片时可指定宽度6 英寸接近 Word 默认版心宽度。表格部分先添加表头行再逐行添加数据。Light Grid Accent 1是内置表格样式排版比默认样式好看得多。5.2 脚本函数化把 2000 行代码拆成可维护模块长脚本最容易出现的问题是“改一处崩全盘”。2000 行的分析脚本如果全部平铺在一个文件里调试效率极低。我一般会按阶段拆成五个函数load_data()负责读文件、clean_data()负责清洗、calc_metrics()负责计算指标、plot_charts()负责出图、generate_report()负责写 Word。每个函数独立测试主函数只负责按顺序调用。def main(): sales_raw, inv_total, inv_valid load_data() cleaned clean_data(sales_raw) metrics calc_metrics(cleaned, inv_total, inv_valid) plot_charts(metrics) generate_report(metrics) if __name__ __main__: main()if __name__ __main__保证了模块被 import 时不会自动执行主流程只在直接运行时触发。这样你可以单独from sales_analysis import clean_data在其他脚本里复用清洗逻辑。每个函数内部可以保留 print 输出关键中间结果便于在人头攒动的 Jupyter Notebook 里分段调试。5.3 图表编号与正文引用图文一致性检查图文分析文档最怕图表的编号和数据与正文对不上。Word 里手工维护编号很容易漏改一个技巧是在生成 docx 时就把图片路径和编号写成一个列表由代码统一推进。chart_meta [ (月度销售额趋势.png, 图1, 月度销售额变化趋势), (品类月度对比.png, 图2, 各品类分月销售额对比), (销售额与环比组合图.png, 图3, 销售额与环比增速组合图), ] for path, label, title in chart_meta: doc.add_paragraph(f{label} {title}) doc.add_picture(path, widthInches(5.5))chart_meta列表集中管理所有图片的文件名、编号和图题生成文档时循环写入。这样做的好处是如果中间删掉一张图只需要改列表Word 里的编号会自动跟随整个循环顺序不会出现“图1 之后直接跳图3”的尴尬。加完之后再人工过一遍正文确保每个“如图所示”后面跟着的编号与列表一致。6. 进阶操作用 Seaborn 热力图做 SKU 关联与图表复用模板6.1 从 pandas 到 seaborn视图升级只需改两行上面用 Matplotlib 完成基础图如果想让图表更好看可以在同一套 DataFrame 上直接切到 seaborn。seaborn 的heatmap特别适合展示“日期 × SKU”的销售热度矩阵或者“月份 × 品类”的销售额热力图信息密度比折线图高很多。下面这段代码以 5 月数据为例看哪些 SKU 在哪些日期是销售主力。import seaborn as sns may_data sales_raw[sales_raw[月份] 2024-05].copy() heat_pivot may_data.pivot_table( index销售日期, columnsSKU, values销售金额, aggfuncsum ).fillna(0) fig, ax plt.subplots(figsize(16, 8)) sns.heatmap(heat_pivot, cmapReds, annotFalse, linewidths0.2, axax) ax.set_title(2024年5月 SKU 日销售金额热力) plt.tight_layout() plt.savefig(SKU热力图.png, dpi200)sns.heatmap的cmapReds让高销售额区域显示深红色可读性好。annotFalse关闭格子里的数字标注因为矩阵太大标了反而看不清。linewidths0.2给格子加了白边区分度更高。如果 SKU 数量多导致 X 轴标签重叠可以每隔 N 个取一个标签或者直接不显示标签只保留色带作为参考。6.2 groupby 排序的隐式坑热力图横纵轴乱序怎么办seaborn 热力图对索引的顺序敏感如果日期列是文本格式“2024-05-01”到“2024-05-31”按字符串排序没问题但如果混入“5月1日”这种文本排序就完全乱掉。使用前必须确认索引已经是 datetime 类型或者用sort_index()强制排序。heat_pivot heat_pivot.sort_index() if isinstance(heat_pivot.index[0], str): heat_pivot.index pd.to_datetime(heat_pivot.index) print(热力图索引类型:, type(heat_pivot.index[0]))sort_index()只对同类型索引有效字符串和 datetime 混用时先统一转类型再排序。pd.to_datetime(heat_pivot.index)会自动推断格式处理5月1日、2024/05/01这类变体基本够用。转完型后热力图的横向时间轴就是严格递增的不会被 Excel 里那些不规则日期文本打乱顺序。6.3 把整套图表模板封装成函数换数据直接复用做过一次完整分析后最有价值的产出不是那份报告而是这套图表代码。下一季度新数据来了可以直接把函数里的 DataFrame 名字换掉其他全部复用。封装时把“输入 DataFrame、输出图片文件”作为函数签名边界。def plot_category_trend(data: pd.DataFrame, output_path: str category_trend.png) - None: required_cols [月份, 商品大类, 销售金额] if not all(col in data.columns for col in required_cols): raise ValueError(f数据缺少必要列需要 {required_cols}) pivot data.pivot_table( index月份, columns商品大类, values销售金额, aggfuncsum ).fillna(0) ax pivot.plot(kindbar, figsize(14, 7), colormapplasma) ax.set_ylabel(销售金额元) ax.set_xlabel(月份) plt.tight_layout() plt.savefig(output_path, dpi200) plt.close()函数开头的required_cols检查非常实用换数据源时如果列名对不上会直接抛异常告诉你缺哪列而不是跑到底才报 KeyError。函数末尾的plt.close()是防止图表对象越积越多导致内存膨胀特别是在循环里反复调用这个函数时不 close 的话最后生成的图片会越来越慢。6.4 数据透视核对的最后一个校验总数一致整个分析流程最后建议做一道总数校验清洗后的明细数据销售额总和与现成日汇总表的销售额总和做差误差控制在 0.01 元以内。这一步通过整个分析链条从数据角度就是闭环的后续写报告引用的任何数字都有可信基础。total_detail sales_raw[销售金额].sum() total_report daily_report[销售金额].sum() diff abs(total_detail - total_report) if diff 0.01: print(f校验通过明细与汇总差异仅 {diff:.4f} 元) else: print(f校验失败差异 {diff:.2f} 元请检查清洗逻辑)这行校验代码虽短但在交付报告时特别有用——评审老师或业务方可能会随手抽查一个月的销售总额如果你明细和汇总对不上整份报告的可信度都会被打折扣。把这步放在脚本最后执行配合前面输出的 Excel 多 sheet 文件整套销售分析大作业就达成了“代码可跑、数据可验、图表可引用、文档可打印”的完整交付状态。本文还有配套的精品资源点击获取