
简介这是一套面向高校学生的Python电商平台数据分析系统完整项目适用于期末大作业、课程设计及毕业设计场景难度适中评审得分达98分内容经助教老师审定。项目围绕电商业务展开涵盖销售趋势分析、复购率计算、用户行为分析、渠道来源统计及RFM用户价值分层等典型分析模块帮助读者掌握从数据清洗到可视化呈现的完整流程。资源包共30个文件以7个Python源码文件为核心辅以19张png结果图表、3个pyc编译文件及1份md说明文档压缩包约1.68MB结构清晰便于按模块查阅。目前已有197人学习浏览。通过这份源码与文档读者可获得可直接运行的完整项目方案、各分析维度的实现思路与图表输出示例以及脏数据处理方式等排错参考适合需要快速搭建数据分析大作业框架或对照学习电商分析逻辑的同学使用。1. 电商数据分析大作业从原始订单表到能答辩的看板中间差了什么每年学期末总有一批同学在群里问同一件事电商平台数据分析系统的源码到底怎么跑起来。标题里写着「高分项目」但真正拿到手才发现数据是假的、图表是静态的、文档说明只有三页截图。我带过几届课程设计也帮人改过不少这类大作业最深的感受是能跑通不等于能答辩能答辩不等于能讲清业务逻辑。这个标题背后真正要解决的问题是把一份散落的订单流水变成一套有清洗、有指标、有可视化、有结论的分析系统。适合谁适合正在做 Python 课程设计、需要交源码和文档、又不想只交一个「能跑但说不清」的项目的同学。接下来我按实际落地顺序把选型、清洗、指标、看板、避坑和进阶一条条拆开讲。2. 技术选型与数据层设计为什么我坚持用 Pandas SQLite 而不是直接上 MySQL2.1 大作业场景下的选型逻辑课程设计的时间窗口通常只有两到三周真正写代码的时间可能不到五天。这时候选型的第一原则不是「生产级」而是环境依赖少、报错可查、答辩时能当场演示。MySQL 当然更贴近企业但安装配置、驱动版本、权限问题随便一个就能吃掉一整天。我一般会建议数据量在十万行以内直接用 SQLite 落库Pandas 做分析Flask 或 Streamlit 出看板。这样整个项目只有三个核心依赖换台电脑也能跑。常见做法是先把原始 CSV 读进 Pandas做一轮清洗后写入 SQLite后续所有指标查询都走 SQL。这样做的好处是清洗逻辑和分析逻辑分离答辩时老师问「数据怎么处理的」你能指着清洗脚本说清楚问「指标怎么算的」你能指着 SQL 说清楚。如果全部塞在 Pandas 里代码会变成一坨自己回头看都费劲。提示不要为了显得「高级」而引入 Spark 或 Hive。数据量撑不起来环境还容易翻车答辩时跑不起来就是灾难。2.2 数据表结构设计与建表脚本电商订单数据通常包含订单号、用户 ID、商品 ID、品类、下单时间、支付时间、金额、数量、省份、渠道等字段。我一般会拆成两张表订单明细表和商品维表。订单明细表存流水商品维表存品类和成本价方便后面算毛利。import sqlite3 import pandas as pd # 连接 SQLite不存在则自动创建 conn sqlite3.connect(ecommerce.db) cursor conn.cursor() # 订单明细表一行代表一个订单中的一个商品 cursor.execute( CREATE TABLE IF NOT EXISTS order_detail ( order_id TEXT, user_id TEXT, product_id TEXT, order_time TEXT, pay_time TEXT, amount REAL, quantity INTEGER, province TEXT, channel TEXT ) ) # 商品维表补充品类和成本用于毛利分析 cursor.execute( CREATE TABLE IF NOT EXISTS product_dim ( product_id TEXT PRIMARY KEY, category TEXT, cost_price REAL ) ) conn.commit() conn.close()这段代码做了两件事建订单明细表和商品维表。order_id和product_id都用 TEXT是因为很多平台的订单号带字母前缀用 INTEGER 会丢精度。order_time和pay_time存字符串后续在 Pandas 里转 datetime 更灵活。amount用 REAL 而不是 DECIMAL是因为 SQLite 对 DECIMAL 支持有限课程设计场景下浮点误差可以接受。参数上唯一需要留意的是quantity有些数据集里退货订单数量是负数清洗阶段要单独处理。建表时不用加索引数据量小加了反而增加写入时间。2.3 从 CSV 到 SQLite 的入库流程拿到原始 CSV 后不要直接to_sql一把梭。先做字段映射和类型转换否则后面 SQL 查询时会出现「金额是字符串没法求和」这种低级问题。import pandas as pd import sqlite3 # 读取原始数据注意编码中文 CSV 常见 gbk 或 utf-8-sig raw pd.read_csv(orders_raw.csv, encodingutf-8-sig) # 字段重命名统一成英文避免后续 SQL 里写中文列名 raw raw.rename(columns{ 订单编号: order_id, 用户ID: user_id, 商品ID: product_id, 下单时间: order_time, 支付时间: pay_time, 实付金额: amount, 购买数量: quantity, 收货省份: province, 来源渠道: channel }) # 类型转换金额转 float数量转 int时间保持字符串 raw[amount] pd.to_numeric(raw[amount], errorscoerce) raw[quantity] pd.to_numeric(raw[quantity], errorscoerce).fillna(0).astype(int) # 丢弃金额为空的行这些通常是未支付或异常订单 raw raw.dropna(subset[amount]) conn sqlite3.connect(ecommerce.db) raw.to_sql(order_detail, conn, if_existsappend, indexFalse) conn.close()逻辑说明encodingutf-8-sig是为了处理 Excel 导出的 CSV 带 BOM 头的问题用 utf-8 会报错。errorscoerce把无法转换的值变成 NaN再统一 drop避免脏数据污染后续统计。if_existsappend而不是replace是为了支持多次追加数据但第一次跑之前要确保表是空的否则会重复。参数上fillna(0)只对 quantity 做因为数量缺失可以当零处理但金额缺失必须丢弃否则会拉低客单价。这一步做完可以用一条 SQL 验证入库行数。SELECT COUNT(*) AS total_rows, COUNT(DISTINCT order_id) AS order_cnt, ROUND(SUM(amount), 2) AS total_gmv FROM order_detail;如果total_rows和原始 CSV 行数差太多说明 dropna 丢多了要回去检查是不是金额列里有「」符号没去掉。3. 数据清洗与指标计算把脏订单变成能算的指标3.1 清洗阶段必须处理的四类脏数据电商数据集的脏通常集中在四个地方重复订单、时间格式混乱、金额带单位、省份名称不统一。我按处理优先级排一下。重复订单最常见的是同一订单号出现多次可能是导出时翻了页。处理方式是按 order_id product_id 去重保留第一条。时间格式混乱表现为「2024/1/5」「2024-01-05」「2024年1月5日」混在一起统一用pd.to_datetime加errorscoerce转换转不了的置空后丢弃。金额带单位就是「99.00」这种用正则去掉非数字字符。省份名称不统一比如「广东」和「广东省」并存用映射表统一。import pandas as pd import re df pd.read_sql(SELECT * FROM order_detail, sqlite3.connect(ecommerce.db)) # 1. 去重同一订单同一商品只保留一条 df df.drop_duplicates(subset[order_id, product_id], keepfirst) # 2. 时间转换统一成 datetime无法转换的丢弃 df[order_time] pd.to_datetime(df[order_time], errorscoerce) df df.dropna(subset[order_time]) # 3. 金额清洗去掉货币符号和千分位 df[amount] df[amount].astype(str).apply( lambda x: re.sub(r[^\d.], , x) ) df[amount] pd.to_numeric(df[amount], errorscoerce) df df.dropna(subset[amount]) # 4. 省份标准化去掉「省」「市」后缀 df[province] df[province].str.replace(r[省市]$, , regexTrue) # 写回清洗后的表 conn sqlite3.connect(ecommerce.db) df.to_sql(order_clean, conn, if_existsreplace, indexFalse) conn.close()这段代码的关键在第三步re.sub(r[^\d.], , x)会把「1,299.00」变成「1299.00」但如果有多个小数点会出问题所以后面还要to_numeric兜底。第四步用正则去掉省市后缀是为了后面按省份聚合时不会出现「广东」和「广东省」两个分组。注意清洗后的表用replace覆盖每次跑都是全新结果。如果数据是分批来的要改成 append 并加去重逻辑。3.2 核心指标的计算口径电商分析绕不开几个指标GMV、客单价、转化率、复购率、品类占比。每个指标的口径必须在文档里写清楚否则答辩时老师一问就露馅。GMV 我一般定义为已支付订单的金额总和不含未支付和退款。客单价 GMV / 去重后的订单数。转化率需要额外的流量数据如果数据集里没有就用「下单用户数 / 访问用户数」代替但要在文档里注明口径。复购率 下单两次及以上的用户数 / 总下单用户数。品类占比 某品类 GMV / 总 GMV。-- GMV 与客单价 SELECT ROUND(SUM(amount), 2) AS gmv, COUNT(DISTINCT order_id) AS order_cnt, ROUND(SUM(amount) / COUNT(DISTINCT order_id), 2) AS avg_order_value FROM order_clean WHERE pay_time IS NOT NULL AND pay_time ! ; -- 复购率 SELECT ROUND( 1.0 * SUM(CASE WHEN order_cnt 2 THEN 1 ELSE 0 END) / COUNT(*), 4 ) AS repurchase_rate FROM ( SELECT user_id, COUNT(DISTINCT order_id) AS order_cnt FROM order_clean GROUP BY user_id ) t; -- 品类 GMV 占比 SELECT p.category, ROUND(SUM(o.amount), 2) AS category_gmv, ROUND(SUM(o.amount) * 100.0 / (SELECT SUM(amount) FROM order_clean), 2) AS pct FROM order_clean o JOIN product_dim p ON o.product_id p.product_id GROUP BY p.category ORDER BY category_gmv DESC;第一条 SQL 里pay_time IS NOT NULL AND pay_time ! 是为了排除未支付订单如果数据集里没有支付时间字段就改用amount 0。第二条复购率用了子查询先算每个用户的订单数再统计大于等于 2 的比例。第三条品类占比用 JOIN 关联维表子查询算总 GMV 做分母。参数上复购率的阈值 2 是行业惯例但课程设计里如果数据量小可以放宽到 1否则复购率可能是 0图表不好看。品类占比保留两位小数答辩时够用。3.3 用 Pandas 做时间维度的趋势分析SQL 适合算静态指标但按天、按周的走势用 Pandas 的 resample 更顺手。我一般会把清洗后的数据读进来按天聚合 GMV再算七天滑动平均这样趋势线不会因为周末波动太剧烈。import pandas as pd import sqlite3 conn sqlite3.connect(ecommerce.db) df pd.read_sql(SELECT order_time, amount FROM order_clean, conn) conn.close() df[order_time] pd.to_datetime(df[order_time]) df df.set_index(order_time) # 按天聚合 GMV daily df.resample(D)[amount].sum().reset_index() daily.columns [date, gmv] # 七天滑动平均平滑周末波动 daily[gmv_ma7] daily[gmv].rolling(window7, min_periods1).mean() # 按周聚合用于周报图表 weekly df.resample(W)[amount].sum().reset_index() weekly.columns [week, gmv] print(daily.tail(10)) print(weekly.tail(5))resample(D)按天重采样缺失日期会自动补零这对趋势图很重要否则折线会断。rolling(window7, min_periods1)算七天滑动平均min_periods1保证前六天也有值不会出现 NaN。resample(W)按周聚合默认周日为一周结束如果课程要求周一到周日加labelleft和closedleft参数。这一步的输出可以直接喂给 Matplotlib 或 Pyecharts后面看板章节会讲怎么接。4. 可视化看板与文档说明让答辩老师三分钟看懂你的项目4.1 用 Streamlit 快速搭一个可交互看板课程设计的看板不需要多华丽但一定要能交互。老师点一下筛选器图表跟着变印象分直接拉满。Streamlit 是我最推荐的工具一个 Python 文件搞定不用写前端。import streamlit as st import pandas as pd import sqlite3 import plotly.express as px st.set_page_config(page_title电商数据分析看板, layoutwide) st.title(电商平台数据分析看板) st.cache_data def load_data(): conn sqlite3.connect(ecommerce.db) df pd.read_sql(SELECT * FROM order_clean, conn) conn.close() df[order_time] pd.to_datetime(df[order_time]) return df df load_data() # 侧边栏筛选器 province st.sidebar.multiselect(选择省份, df[province].unique()) channel st.sidebar.multiselect(选择渠道, df[channel].unique()) filtered df.copy() if province: filtered filtered[filtered[province].isin(province)] if channel: filtered filtered[filtered[channel].isin(channel)] # 核心指标卡片 col1, col2, col3 st.columns(3) col1.metric(GMV, f{filtered[amount].sum():,.2f}) col2.metric(订单数, filtered[order_id].nunique()) col3.metric(客单价, f{filtered[amount].sum() / filtered[order_id].nunique():,.2f}) # 按天 GMV 趋势 daily filtered.set_index(order_time).resample(D)[amount].sum().reset_index() fig px.line(daily, xorder_time, yamount, title每日 GMV 趋势) st.plotly_chart(fig, use_container_widthTrue) # 品类占比 category filtered.groupby(product_id)[amount].sum().reset_index() fig2 px.pie(category, valuesamount, namesproduct_id, title商品 GMV 占比) st.plotly_chart(fig2, use_container_widthTrue)st.cache_data是必须加的否则每次筛选都重新读数据库页面会卡。st.columns(3)做指标卡片st.sidebar.multiselect做筛选器plotly.express出交互图。use_container_widthTrue让图表自适应宽度避免在投影仪上显示不全。参数上resample(D)和前面一致。如果数据量超过十万行load_data里要加parse_dates参数否则 datetime 转换会慢。4.2 文档说明该写什么才不被扣分很多同学把源码交上去文档只有「运行步骤」和「截图」结果被扣了格式分。我一般会按这个结构写项目概述、技术栈、数据说明、模块设计、核心代码说明、运行步骤、指标口径、不足与改进。其中指标口径和不足与改进是加分项说明你思考过边界。数据说明里要写清楚数据来源、字段含义、数据量、清洗规则。模块设计画一个简单的分层图用文字描述也行不要用 mermaid。核心代码说明挑三到四个关键函数贴代码加注释不要全文粘贴。提示文档里的截图要带时间戳和运行环境比如「Windows 11 Python 3.10」这样老师知道你是真跑过。4.3 答辩演示的脚本化流程答辩时间通常只有五到八分钟现场跑代码容易出意外。我一般会提前把看板跑起来用浏览器开好页面演示时只做三件事展示筛选交互、展示核心指标、展示一个业务结论。业务结论要提前从数据里挖出来比如「广东省 GMV 占比最高但客单价低于均值说明该地区以低价走量为主」。这种结论比「我用了 Pandas」有说服力得多。演示前把数据库文件、CSV、代码、文档放在同一个文件夹用相对路径引用避免换电脑后路径报错。如果老师要求现场跑提前在教室电脑上装好依赖或者准备一个 requirements.txt。5. 避坑与常见问题那些让我熬夜的报错和逻辑错误5.1 中文编码导致读取失败现象pd.read_csv报UnicodeDecodeError: utf-8 codec cant decode byte。原因Excel 导出的 CSV 默认是 gbk 或 utf-8-sig不是纯 utf-8。解决先试encodingutf-8-sig不行再试gbk还不行用chardet检测。5.2 金额列是字符串导致求和结果为 0现象SUM(amount)返回 0 或者拼接成一长串数字。原因CSV 里金额带「」或千分位逗号Pandas 读进来是 object 类型。解决入库前用正则清洗或者 SQL 里用CAST(REPLACE(REPLACE(amount, , ), ,, ) AS REAL)。我一般在前置清洗阶段就处理掉不留给 SQL。5.3 时间字段格式不统一导致 resample 报错现象resample(D)报TypeError: Only valid with DatetimeIndex。原因order_time 列还是字符串没有转成 datetime。解决pd.to_datetime(df[order_time], errorscoerce)转换后 dropna。如果格式特别乱加formatmixed参数让 Pandas 自动推断。5.4 复购率算出来是 0 或 1现象复购率要么 0 要么 100%。原因用户 ID 列有空格或大小写不一致导致同一用户被当成多个。解决df[user_id] df[user_id].str.strip().str.lower()。另外检查是不是数据量太小只有几十个用户复购率本来就不稳定。5.5 Streamlit 页面每次筛选都重新读库现象点一下筛选器页面卡三秒。原因没有加缓存每次交互都重新执行read_sql。解决在数据加载函数上加st.cache_data并把筛选逻辑放在缓存之后。如果数据会变加ttl600设置十分钟过期。6. 进阶技巧把静态指标变成可解释的业务结论6.1 用 RFM 模型给用户分层基础指标做完后如果想拿更高分可以加一个 RFM 分层。R 是最近一次购买时间F 是购买频次M 是消费金额。每个维度按四分位数打分组合成八类用户比如「重要价值客户」「一般保持客户」。import pandas as pd import sqlite3 conn sqlite3.connect(ecommerce.db) df pd.read_sql(SELECT user_id, order_time, order_id, amount FROM order_clean, conn) conn.close() df[order_time] pd.to_datetime(df[order_time]) snapshot df[order_time].max() pd.Timedelta(days1) rfm df.groupby(user_id).agg({ order_time: lambda x: (snapshot - x.max()).days, order_id: nunique, amount: sum }).reset_index() rfm.columns [user_id, recency, frequency, monetary] # 按四分位数打分1 到 4 分 rfm[r_score] pd.qcut(rfm[recency], 4, labels[4, 3, 2, 1]).astype(int) rfm[f_score] pd.qcut(rfm[frequency].rank(methodfirst), 4, labels[1, 2, 3, 4]).astype(int) rfm[m_score] pd.qcut(rfm[monetary], 4, labels[1, 2, 3, 4]).astype(int) # 组合成 RFM 总分 rfm[rfm_score] rfm[r_score].astype(str) rfm[f_score].astype(str) rfm[m_score].astype(str) print(rfm.head(10)) print(rfm[rfm_score].value_counts().head(10))snapshot取最大日期加一天避免 recency 为 0。pd.qcut按四分位数切分labels的顺序要注意recency 越小越好所以标签是 [4,3,2,1]frequency 和 monetary 越大越好标签是 [1,2,3,4]。rank(methodfirst)是为了处理频次相同的用户避免 qcut 报边界重复错误。跑完这一步你可以把 rfm_score 前几类用户拎出来在答辩时说「这三类用户贡献了 60% 的 GMV建议重点维护」这就是业务结论。6.2 用同期群分析看留存同期群Cohort分析能看出不同月份进来的用户后续留存怎么样。做法是按用户首单月份分组再看他们在后续月份的下单比例。import pandas as pd df[order_month] df[order_time].dt.to_period(M) df[cohort] df.groupby(user_id)[order_month].transform(min) cohort_data df.groupby([cohort, order_month])[user_id].nunique().reset_index() cohort_pivot cohort_data.pivot(indexcohort, columnsorder_month, valuesuser_id) # 转成留存率 cohort_size cohort_pivot.iloc[:, 0] retention cohort_pivot.divide(cohort_size, axis0) print(retention.round(3))transform(min)取每个用户的首单月份作为同期群标签。pivot后第一列是群规模后面每列除以群规模就是留存率。这个表在答辩时展示比单纯说「复购率 30%」更有层次。6.3 我踩过的两个坑和一条习惯第一个坑是 qcut 报「Bin edges must be unique」原因是数据里大量重复值四分位数切出来边界一样。解决办法是用rank(methodfirst)先打散或者改用pd.cut手动指定边界。第二个坑是 Streamlit 的st.cache_data和数据库连接混用导致连接对象被缓存后失效。解决办法是缓存数据而不是连接每次函数内部重新建连接。我现在养成的习惯是每写完一个指标先用一条最简单的 SQL 验证总数再和 Pandas 的结果对一遍。两边对不上一定是清洗阶段漏了东西。这个习惯帮我省了无数个熬夜查 bug 的晚上。希望帮到你。本文还有配套的精品资源点击获取