简介:这是一份面向计算机、自动化等专业本科生的Python课程大作业级电商平台数据分析系统实践资源,聚焦真实业务场景下的数据清洗、分析与可视化全流程训练。资源包含完整可运行源码、分模块说明及图文并茂的使用教程,适用于期末课程设计、课程大作业或毕业设计参考,具备扎实的工程实践与教学适配价值。压缩包共30个文件,含7个核心Python脚本(如SalesTrend.py、RFM.py、UserBehavior2.py等,覆盖销售趋势、用户复购、行为路径与RFM客户分群)、19张分析结果PNG图表(含脏数据处理示意图、RFM模型图、多维度趋势图等),以及项目说明.md文档,整体仅1.68MB,轻量易部署。已有884人学习下载,代码经实际运行验证,附带清晰的字段映射表、2020年数据筛选逻辑、异常支付时间与负金额订单的清洗策略,以及从原始订单到分析报告的端到端实现路径,助学习者快速掌握电商数据分析典型范式。
1. 为什么一个电商数据分析系统的大作业,90%的学生卡在“数据没动起来”这一步?
这不是一个讲 Python 语法的课设,而是一次对真实业务链路的微型复刻:从模拟订单、用户、商品三张核心表出发,用 Pandas 做清洗与关联,用 Matplotlib/Seaborn 画出能被业务方一眼看懂的销售热力图、复购漏斗、用户分层雷达图,最后用 Flask 搭个轻量后台,把分析结果变成可交互的网页报表。它不追求高并发或微服务,但要求你亲手打通「原始数据 → 清洗逻辑 → 统计口径 → 可视化表达 → Web 展示」的全闭环。适合刚学完 Pandas 基础、想验证自己能不能把课堂知识串成一条线的本科生;也适合需要快速交付一个“有数据、有图表、有界面”的课程成果,又不想堆砌空洞代码的同学。它不是炫技项目,而是检验你是否真正理解“数据分析”四个字落在实操中意味着什么——不是调一个.plot()就完事,而是得知道为什么这张表要按user_id去重、为什么order_time必须转为 datetime 才能算周环比、为什么漏斗里第二步的分母必须是上一步的去重用户数。很多同学解压源码后直接运行app.py,看到报错KeyError: 'product_category'就停了,其实问题根本不在代码,而在你还没打开data/目录下的 CSV 文件,看看字段名是不是被 Excel 自动改成了product_category.1。
2. 从零跑通:用 3 个脚本完成数据准备、分析逻辑、Web 展示的最小闭环
这个系统不是“写完就交”,而是“跑通才及格”。我一般会把它拆成三个独立可验证的阶段:先让数据动起来(prepare_data.py),再让指标算出来(analyze.py),最后让图表亮起来(app.py)。每个阶段失败,都不影响其他阶段调试——这是避免“一崩全崩”的血泪经验。下面每一步都带可复制命令、关键参数说明和失败时该盯哪一行日志。
2.1 用prepare_data.py把杂乱 CSV 整成标准宽表:字段对齐、时间标准化、空值策略落地
很多同学跳过这步,直接拿原始 CSV 喂给分析脚本,结果groupby报错、merge出现笛卡尔积、时间切片全是NaT。prepare_data.py的核心任务不是“读进来再存出去”,而是做三件事:统一字段命名(比如把user_id,uid,customer_no全映射为user_id)、强制类型转换(特别是order_time必须是datetime64[ns],否则dt.weekofyear会静默失败)、定义空值处理规则(product_category缺失时填"未知",但order_amount缺失则整行丢弃——因为金额缺失无法补全)。
# prepare_data.py import pandas as pd import numpy as np # 1. 读取三张原始表(注意编码,Windows下常为gbk) users = pd.read_csv("data/users.csv", encoding="gbk") orders = pd.read_csv("data/orders.csv", encoding="gbk") products = pd.read_csv("data/products.csv", encoding="gbk") # 2. 字段标准化:统一列名,只保留分析必需字段 users = users.rename(columns={"uid": "user_id", "reg_date": "register_time"}) orders = orders.rename(columns={"order_no": "order_id", "create_time": "order_time"}) products = products.rename(columns={"pid": "product_id", "cate": "product_category"}) # 3. 时间列强制转换(关键!失败时这里会报错) orders["order_time"] = pd.to_datetime(orders["order_time"], errors="coerce") # errors="coerce" 表示非法时间转为 NaT,后续可查:orders[orders["order_time"].isna()] # 4. 空值处理:分类填充 vs 丢弃 orders = orders.dropna(subset=["order_amount"]) # 金额缺失无法补,直接删 products["product_category"] = products["product_category"].fillna("未知") # 5. 关联成宽表(左连接保证用户维度不丢失) df = orders.merge(users[["user_id", "gender", "age"]], on="user_id", how="left") df = df.merge(products[["product_id", "product_category"]], on="product_id", how="left") # 6. 保存为标准中间表 df.to_csv("data/processed_orders.csv", index=False, encoding="utf-8-sig") print(f"✅ 处理完成:共 {len(df)} 条有效订单,{df['user_id'].nunique()} 个用户")提示:
encoding="utf-8-sig"是为防止 Windows Excel 打开 CSV 时中文乱码;errors="coerce"不是偷懒,而是把时间解析失败暴露为NaT,方便你用df[df.order_time.isna()]定位脏数据源。如果这一步报ParserError,大概率是 CSV 里有未转义的逗号或换行符,用文本编辑器打开orders.csv第 127 行(报错行号)就能看到。
2.2 用analyze.py计算 5 个硬指标:复购率、品类集中度、客单价分布、新老客贡献比、时间趋势
分析脚本不是“画图前奏”,而是业务逻辑的翻译器。比如“复购率”不是len(df[df.user_id.duplicated()]) / len(df),而是len(df.groupby('user_id').filter(lambda x: len(x) >= 2).user_id.unique()) / df.user_id.nunique()——前者算的是“有重复订单的用户占总订单比例”,后者才是“有复购行为的用户占总用户比例”。本系统聚焦 5 个高频课程考核点,每个指标都附带业务含义和防错校验:
| 指标名 | 计算逻辑(Pandas) | 业务含义 | 防错校验点 |
|---|---|---|---|
| 月度GMV趋势 | df.groupby(df.order_time.dt.to_period("M"))["order_amount"].sum().sort_index() | 看销售是否健康增长,识别淡旺季 | 检查order_time是否全为NaT(说明 prepare_data.py 时间转换失败) |
| TOP5品类销售额占比 | df.groupby("product_category")["order_amount"].sum().nlargest(5).div(df.order_amount.sum()).round(3) | 判断品类结构是否健康,避免过度依赖单一品类 | 若"未知"占比超 40%,需回查products.csv中cate字段是否大量为空 |
| 新客/老客GMV贡献比 | new_users = df[df.order_time < df.register_time + pd.Timedelta(days=7)]new_gmv = new_users.order_amount.sum()old_gmv = df[~df.index.isin(new_users.index)].order_amount.sum() | 新客拉新效率 vs 老客留存价值 | register_time必须存在且为 datetime,否则+ pd.Timedelta报错 |
| 客单价分布(箱线图基础) | df.groupby("user_id")["order_amount"].sum().describe([0.25,0.5,0.75,0.9]) | 识别价格敏感型用户群,支撑定价策略 | 若user_id为空,groupby返回空 Series,.describe()报ValueError |
| 复购用户画像 | repurchase_users = df.groupby("user_id").filter(lambda x: len(x) >= 2).user_id.unique()df[df.user_id.isin(repurchase_users)][["gender","age"]].value_counts(normalize=True).round(2) | 复购主力是哪类人?指导精准营销 | filter后若返回空数组,说明数据中无复购行为(样本太小或时间跨度不足) |
# analyze.py(节选:复购率与新老客GMV计算) import pandas as pd import numpy as np df = pd.read_csv("data/processed_orders.csv", parse_dates=["order_time", "register_time"]) # ✅ 复购率:有≥2笔订单的用户数 / 总用户数 repurchase_user_count = df.groupby("user_id").filter(lambda x: len(x) >= 2).user_id.nunique() total_user_count = df.user_id.nunique() repurchase_rate = repurchase_user_count / total_user_count if total_user_count > 0 else 0 print(f"📊 复购率:{repurchase_rate:.1%}({repurchase_user_count}/{total_user_count})") # ✅ 新客GMV:注册后7天内首单金额(注意:不是所有用户都有首单时间!) # 先确保 register_time 有效,再计算首单时间 valid_users = df.dropna(subset=["register_time"]) valid_users["first_order_time"] = valid_users.groupby("user_id")["order_time"].transform("min") valid_users["is_new_customer"] = (valid_users["first_order_time"] - valid_users["register_time"]) <= pd.Timedelta(days=7) new_gmv = valid_users[valid_users["is_new_customer"]]["order_amount"].sum() old_gmv = valid_users[~valid_users["is_new_customer"]]["order_amount"].sum() print(f"💰 新客GMV:¥{new_gmv:,.0f},老客GMV:¥{old_gmv:,.0f},占比 {new_gmv/(new_gmv+old_gmv):.1%}")参数说明:
pd.Timedelta(days=7)是课程常见设定,实际业务中可能是 30 天;transform("min")比agg("min")更安全,因为它保持原 DataFrame 行数,避免merge时索引错位;valid_users = df.dropna(...)是防御性编程——如果register_time大量缺失,直接groupby会把缺失值当一个组,导致新客计算完全错误。
2.3 用app.py启动 Flask 服务:3 个路由支撑 5 张核心图表,不依赖数据库
Web 层的目标不是炫酷 UI,而是证明“分析结果能被非技术人员查看”。本系统用 Flask + Jinja2 模板,所有数据在内存中计算并传入模板,不连 MySQL/SQLite——降低部署门槛,也符合课程“单机可运行”要求。三个核心路由对应三种使用场景:首页展示汇总卡片(GMV、复购率等 KPI)、/trend展示时间趋势折线图、/category展示品类 TOP5 饼图。关键在于:图表生成必须在路由函数内完成,不能全局变量缓存(多用户访问会冲突),且图片必须转为 base64 嵌入 HTML(避免静态文件路径问题)。
# app.py(精简版,仅含 /trend 路由) from flask import Flask, render_template import pandas as pd import matplotlib.pyplot as plt import io import base64 app = Flask(__name__) @app.route('/trend') def show_trend(): # 1. 重新加载处理后的数据(每次请求都读,保证数据最新) df = pd.read_csv("data/processed_orders.csv", parse_dates=["order_time"]) # 2. 计算月度GMV(同 analyze.py 逻辑,保持一致性) monthly_gmv = df.groupby(df.order_time.dt.to_period("M"))["order_amount"].sum().sort_index() # 3. 用 matplotlib 画图(注意:必须关闭 interactive mode) plt.figure(figsize=(10, 4)) monthly_gmv.plot(kind="line", marker="o", color="#1f77b4") plt.title("月度销售GMV趋势", fontsize=14) plt.ylabel("销售额(元)") plt.grid(True, alpha=0.3) # 4. 将图转为 base64 字符串嵌入 HTML img_buffer = io.BytesIO() plt.savefig(img_buffer, format='png', bbox_inches='tight') img_buffer.seek(0) img_base64 = base64.b64encode(img_buffer.read()).decode() plt.close() # ⚠️ 必须关闭,否则内存泄漏 # 5. 将 base64 字符串传给模板 return render_template("trend.html", chart_img=img_base64, gmv_list=monthly_gmv.round(0).astype(int).tolist(), month_list=[str(m) for m in monthly_gmv.index]) if __name__ == '__main__': app.run(debug=True, host='0.0.0.0', port=5000)逻辑说明:
plt.close()是隐藏大坑——不关的话,每次请求都会新建一个 figure 对象,100 个并发请求就占满内存;io.BytesIO()替代plt.savefig("static/trend.png"),彻底规避静态文件路径配置;gmv_list和month_list传入模板是为了在 HTML 中渲染数据表格(如“2023-01: ¥12,500”),让图表和数字一一对应,避免“图是图、数是数”的割裂感。
3. 数据清洗避坑:5 个让 80% 同学调试超 2 小时的致命细节
别怪数据不听话,往往是清洗时埋下的雷在半夜爆炸。以下是我带过 3 届课程设计后,整理出的最高频、最隐蔽、最让人抓狂的 5 个坑。每一条都来自真实翻车现场,不是理论推测。
3.1 现象:orders.csv用 Excel 打开正常,但pd.read_csv()报ParserError: Error tokenizing data
原因:Excel 自动将长数字(如订单号123456789012345)转为科学计数法1.23E+14,保存 CSV 时写入的就是123000000000000,且原始字符串中的逗号、双引号未转义。Pandas 解析时遇到未闭合引号或意外逗号就崩溃。
解决:用记事本或 VS Code 打开orders.csv,搜索"或,出现在字段中间的位置(如"iPhone, 14 Pro");用pd.read_csv(..., quotechar='"', escapechar='\\')显式指定引号和转义符;终极方案:让数据提供方导出为 UTF-8 编码的纯文本 CSV,禁用 Excel 自动格式化。
3.2 现象:df.groupby("user_id").size()返回的用户数,比len(df.user_id.unique())少 20%
原因:user_id字段混有空格或不可见字符(如\xa0),"A123 "和"A123"被视为不同用户。unique()会去重,但groupby默认不 strip 空格。
解决:清洗时强制df["user_id"] = df["user_id"].astype(str).str.strip();检查异常值:df.user_id.str.len().describe(),若出现min=0或max>20,大概率有脏数据;用df.user_id.apply(lambda x: repr(x))查看真实字符串('A123\xa0'会显示为'A123\xa0')。
3.3 现象:df.order_time.dt.month返回全是1,但原始数据明明有 3 月、6 月订单
原因:order_time列被pd.read_csv()错误识别为object类型,而非datetime64。.dt.month对 object 列静默返回1(Pandas 的玄学行为)。
解决:清洗后立刻验证df.order_time.dtype,必须是datetime64[ns];若为object,用pd.to_datetime(df.order_time, errors="coerce")并检查df.order_time.isna().sum();在prepare_data.py开头加断言:assert df.order_time.dtype == "datetime64[ns]"。
3.4 现象:df.merge(products, on="product_id")后行数暴涨 10 倍,出现大量重复记录
原因:products.csv中product_id不唯一(同一商品多次录入),或orders.csv中product_id有拼写错误(如"P001 "vs"P001"),导致merge时产生笛卡尔积。
解决:合并前先检查主键唯一性:products.product_id.is_unique(应为True);用orders.product_id.isin(products.product_id).sum() / len(orders)计算匹配率,低于 95% 就要查错;对products去重:products = products.drop_duplicates(subset=["product_id"])。
3.5 现象:Flask启动后访问/trend页面空白,控制台无报错
原因:Matplotlib 默认后端是TkAgg,在无图形界面的服务器环境(如某些 Linux 云主机)会崩溃,但错误被静默吞掉。页面返回空响应。
解决:在app.py最开头插入:
import matplotlib matplotlib.use('Agg') # 强制使用非交互后端 import matplotlib.pyplot as plt这是后悔药——不加这行,所有plt相关代码在无 GUI 环境下都会失效,且不报错。
4. 图表表达避坑:3 个让老师皱眉的“伪专业”可视化陷阱
图表不是越花哨越好,而是越准确、越易读、越契合业务目标越好。课程设计里常见的“看起来很专业,实则误导人”的陷阱,往往源于对 Matplotlib 默认参数的盲目信任。
4.1 饼图占比总和≠100%?那是autopct四舍五入惹的祸
现象:TOP5 品类饼图上,各 slice 标注25.3%,18.7%,15.2%,12.1%,10.5%,加起来是81.8%,剩下18.2%被标为"其他",但图例里"其他"却没显示。
原因:autopct="%.1f%%"对每个 slice 单独四舍五入,5 个数相加必然有误差;explode参数若设为(0.1,0,0,0,0),只突出第一个 slice,但"其他"slice 因数值小被压缩到看不见。
解决:用pctdistance=0.85(百分比标签离圆心更近,避免重叠);手动计算"其他"占比并显式加入数据:
# 计算TOP5,剩余归为"其他" top5 = df.groupby("product_category")["order_amount"].sum().nlargest(5) others = df.groupby("product_category")["order_amount"].sum().sum() - top5.sum() labels = list(top5.index) + ["其他"] sizes = list(top5.values) + [others] plt.pie(sizes, labels=labels, autopct=lambda p: f'{p:.1f}%' if p > 5 else '', startangle=90, pctdistance=0.85)技巧:
autopct=lambda p: f'{p:.1f}%' if p > 5 else ''表示占比小于 5% 的 slice 不显示数字,避免图表拥挤。
4.2 折线图 X 轴日期挤成一团?那是plt.xticks()没做旋转
现象:月度趋势图 X 轴显示2023-01, 2023-02, 2023-03...,但所有标签重叠成一条黑线,看不出月份。
原因:Matplotlib 默认不旋转日期标签,且自动选择的刻度太密。
解决:用plt.xticks(rotation=45)强制旋转;用plt.gca().xaxis.set_major_locator(plt.MaxNLocator(6))限制最多显示 6 个刻度:
monthly_gmv.plot(kind="line", marker="o") plt.xticks(rotation=45) plt.gca().xaxis.set_major_locator(plt.MaxNLocator(6)) # 关键! plt.tight_layout() # 防止标签被截断4.3 漏斗图第二步转化率 120%?那是分母用了错误的用户基数
现象:漏斗步骤为 “浏览→加购→下单→支付”,计算加购人数 / 浏览人数 = 120%,明显违反常识。
原因:浏览人数统计的是page_view日志中的 UV(去重用户数),但加购人数统计的是cart_add表中的行数(同一用户多次加购算多次),分母分子口径不一致。
解决:所有漏斗步骤必须用同一维度的去重用户数:
# 正确做法:每一步都基于 user_id 去重 funnel_steps = { "浏览": page_views["user_id"].nunique(), "加购": cart_adds["user_id"].nunique(), # 不是 len(cart_adds) "下单": orders["user_id"].nunique(), "支付": paid_orders["user_id"].nunique() } # 转化率 = 当前步 / 上一步 conversion_rates = {k: v/funnel_steps[prev_k] if i>0 else 1.0 for i, (k, v) in enumerate(funnel_steps.items()) for prev_k in [list(funnel_steps.keys())[i-1]] if i>0}5. 交付前必做的 3 项验证:让老师一眼看出“这活儿你真干了”
课程设计的终极目标不是“能跑”,而是“让老师信服你理解了”。以下三项验证,每做完一项,你的报告可信度就提升一个档次。它们不增加代码量,但直击评审核心诉求:数据是否真实、逻辑是否自洽、结论是否可解释。
5.1 交叉验证:用 Excel 手动验算 1 个核心指标,截图附在报告里
选一个最简单的指标,比如“2023年3月GMV”。在processed_orders.csv中用 Excel 筛选order_time在2023-03-01到2023-03-31的行,对order_amount求和,截图保存为excel_gmv_march.png。再运行analyze.py,对比控制台输出的2023-03数值。两者必须完全一致(小数点后一位)。这证明你的清洗逻辑没篡改数据,pd.to_datetime和dt.to_period工作正常。老师看到这张图,就知道你不是 copy-paste 代码,而是亲手摸过数据。
5.2 边界测试:故意删掉 10 行数据,看指标变化是否符合业务直觉
备份原始orders.csv为orders_full.csv,然后删掉最后 10 行(模拟数据不全场景),运行全流程。观察:
- 复购率是否小幅下降(因为删掉的可能是复购用户的第二单)?
- TOP5 品类排名是否不变,但
"未知"占比是否上升(因为删掉的可能是已标注品类的订单)?
如果变化方向反直觉(如删数据后复购率暴涨),说明你的复购逻辑有硬伤(比如用了duplicated(keep=False)而非filter(lambda x: len(x)>=2))。这种测试逼你思考“我的代码在数据变少时,行为是否合理”。
5.3 业务解读:在报告里写一句“这个数字说明了什么”,而不是“这个数字是XXX”
不要写:“复购率是 32.5%”。要写:“复购率 32.5% 意味着每 3 个新用户中,就有 1 个会在 30 天内再次下单;结合客单价 ¥156,说明老客生命周期价值(LTV)至少是新客获客成本(CAC)的 2 倍——建议运营团队将预算向会员体系倾斜。” 这句话不需要你真算 LTV/CAC,但展示了你把数字放回业务语境的能力。老师批改时,扫一眼这种句子,就知道你不是在堆代码。
我带过的某高校课程设计中,A同学的报告里有一张手绘的“用户旅程图”,标注了从注册到复购的平均天数(14.2 天)、流失节点(加购后 48 小时未下单)、挽回策略(发优惠券),虽然图是用 PPT 画的,但老师给了最高分——因为那张图证明他真的在用数据思考“人”怎么用产品,而不是“表”怎么连表。希望帮到你。
本文还有配套的精品资源,点击获取