ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

脱敏B2C电商数据集实战:从字段语义到宽表构建与场景分析

脱敏B2C电商数据集实战:从字段语义到宽表构建与场景分析 简介这份数据集来自国内某B2C电子商务网站面向数据分析、数据挖掘与机器学习方向的学习者和研究者可用于用户行为分析、商品推荐、订单预测、客户分层等实战场景适合具备一定SQL与数据处理基础的中高级读者。资源包共30个文件涵盖xml、csv、sql、xls、pdf及dmp等多种格式其中csv与sql文件提供买家信息、商品库、订单及明细、行为日志、收藏夹、商品访问等核心业务表xml与pdf用于补充数据结构说明dmp为Oracle格式的完整版数据压缩包整体约187.75MB。数据规模包含15万顾客信息、1.1万件商品、28万条顾客行为日志、4万条订单及24万件商品交易记录另附充值历史、退货订单、订单处理日志、消费金额汇总、到货提醒等衍生数据。目前已有514人学习下载可帮助读者快速构建贴近真实电商业务的分析环境完成从数据清洗、指标建模到可视化呈现的完整链路练习。1. 拿到一份脱敏 B2C 电商数据集先别急着跑模型很多人拿到「国内某 B2C 电子商务网站的数据集不含隐私」这类资源第一反应是打开 Jupyter 直接read_csv然后groupby看销售额。我踩过这个坑一份字段名全是拼音缩写、时间戳混着字符串、订单表和退款表主键对不上的数据集硬跑出来的 GMV 能比真实值虚高 30%。脱敏数据集的价值不在于「有没有隐私字段」而在于它保留了完整的行为链路结构——用户从浏览、加购、下单、支付到退款每一环都有可追溯的 ID 和时间戳。这篇文章面向两类人一是想拿电商数据练手但找不到干净数据源的算法工程师二是需要做经营分析却拿不到生产库权限的数据分析师。我会按「先验字段语义 → 再建宽表 → 后做场景验证」的顺序把这份数据集从裸表到能产出结论的全过程拆开讲包括字段映射的猜法、时间对齐的坑、以及怎么用最小成本验证数据质量。2. 先搞懂脱敏电商数据集的字段语义与表关系2.1 脱敏不是删列而是替换标识符国内 B2C 数据集的脱敏方式通常是保留行为字段、替换主体标识。用户 ID 会变成u_加哈希串商品 ID 变成纯数字但打乱了原始顺序收货地址只留到城市级别。这意味着你没法做用户画像的跨表关联还原但用户行为序列的时序关系是完整的。常见表结构包括用户行为日志表曝光、点击、加购、收藏、订单主表订单号、用户 ID、下单时间、支付时间、订单状态、订单明细表订单号、商品 ID、数量、单价、优惠金额、商品维表商品 ID、类目、品牌、上架时间、退款表订单号、退款时间、退款金额、退款原因码。这五张表通过user_id、order_id、item_id三个键串联构成电商分析的最小闭环。提示拿到数据第一件事是确认order_id在订单主表和明细表里是否一一对应。我遇到过明细表里一个订单号出现 47 行的情况原因是把赠品也拆成了独立行如果不做聚合直接 join 会导致金额翻倍。2.2 用三行 Pandas 代码摸清表结构不要凭字段名猜含义先跑一遍基础统计。下面这段代码输出每张表的行数、主键唯一值数量、时间字段范围能快速判断数据是否可用。import pandas as pd # 替换为你的实际文件路径注意 encoding 常见为 gbk 或 utf-8-sig tables { behavior: pd.read_csv(behavior_log.csv, encodinggbk), orders: pd.read_csv(order_main.csv, encodinggbk), order_items: pd.read_csv(order_detail.csv, encodinggbk), items: pd.read_csv(item_info.csv, encodinggbk), refunds: pd.read_csv(refund.csv, encodinggbk), } for name, df in tables.items(): print(f {name} ) print(f行数: {len(df)}, 列数: {df.shape[1]}) print(f列名: {list(df.columns)}) # 自动识别时间列并输出范围 for col in df.columns: if time in col.lower() or date in col.lower(): try: parsed pd.to_datetime(df[col], errorscoerce) print(f {col} 范围: {parsed.min()} ~ {parsed.max()}, 空值率: {parsed.isna().mean():.2%}) except Exception: pass print()这段代码的关键在errorscoerce脱敏数据的时间字段经常混入0000-00-00或空字符串强制转换后变成NaT空值率超过 5% 就要警惕。另一个参数是encoding国内数据集用gbk的概率高于utf-8如果报UnicodeDecodeError就换gb18030再试。输出结果里重点看三件事订单表的时间范围是否覆盖行为日志的时间范围、商品表的item_id是否完全包含明细表里出现的商品、退款表的订单号是否是订单主表的子集。这三个关系决定了后续 join 的方向和过滤条件。2.3 字段映射的猜法与验证脱敏后的字段名常见两种风格拼音首字母缩写如ddh代表订单号、yhid代表用户 ID和英文简写如ord_no、usr_id。如果列名完全不可读用值分布反推订单号列通常是长数字串且唯一值接近行数用户 ID 列的唯一值数量远小于行数金额列的小数位通常是两位状态列只有个位数取值。下面这个函数可以批量输出候选字段的特征。def profile_columns(df, max_unique20): 输出每列的类型、唯一值数量、样例值辅助判断字段含义 for col in df.columns: nunique df[col].nunique() samples df[col].dropna().unique()[:3] print(f{col:20s} | dtype{str(df[col].dtype):10s} | nunique{nunique:8d} | samples{samples}) # 对订单主表执行 profile_columns(tables[orders])判断逻辑nunique等于行数的列大概率是主键nunique在 3 到 10 之间的列是状态枚举dtype为 float 且最大值在合理客单价范围内的列是金额dtype为 object 但样例值全是数字字符串的列需要转数值。我一般会先跑一遍这个函数把每列的猜测含义写进一个field_mapping.json后续所有分析都引用这个映射文件避免中途改字段名导致代码全废。3. 构建订单宽表从五张裸表到一张分析底表3.1 宽表设计的三个原则电商分析的宽表不是越宽越好。我的原则是一行一个订单明细、保留用户和商品的关键属性、时间字段拆成下单和支付两个。具体来说以订单明细表为基准左连接订单主表拿到用户 ID 和订单状态左连接商品维表拿到类目和品牌左连接退款表拿到退款标记。行为日志表不直接进宽表因为行为是用户粒度的时序数据强行聚合到订单粒度会丢失信息正确做法是单独做用户行为特征表再按用户 ID 关联。注意连接顺序很重要。如果先连接退款表再连接商品表退款表里一个订单可能对应多条退款记录会导致商品金额被重复计算。正确顺序是先聚合退款表到订单粒度再参与连接。3.2 宽表构建的完整代码与参数说明import pandas as pd import numpy as np # 1. 退款表聚合到订单粒度一个订单多次退款只算总退款金额 refund_agg tables[refunds].groupby(order_id).agg( refund_amount(refund_amount, sum), refund_cnt(refund_id, count), first_refund_time(refund_time, min) ).reset_index() # 2. 订单主表选取必要列避免列名冲突 order_main tables[orders][[ order_id, user_id, order_time, pay_time, order_status, city_level ]].copy() # 3. 商品维表选取类目和品牌 item_info tables[items][[item_id, category_l1, category_l2, brand, list_price]].copy() # 4. 以明细表为基准逐步左连接 wide tables[order_items].merge(order_main, onorder_id, howleft) wide wide.merge(item_info, onitem_id, howleft) wide wide.merge(refund_agg, onorder_id, howleft) # 5. 填充退款缺失值为 0并生成退款标记 wide[refund_amount] wide[refund_amount].fillna(0) wide[refund_cnt] wide[refund_cnt].fillna(0) wide[is_refund] (wide[refund_amount] 0).astype(int) # 6. 时间字段统一转换计算支付时长 for col in [order_time, pay_time, first_refund_time]: wide[col] pd.to_datetime(wide[col], errorscoerce) wide[pay_duration_min] (wide[pay_time] - wide[order_time]).dt.total_seconds() / 60 # 7. 计算实付金额单价 × 数量 - 优惠分摊 wide[actual_amount] wide[unit_price] * wide[quantity] - wide[discount_amount].fillna(0) print(f宽表行数: {len(wide)}, 列数: {wide.shape[1]}) print(f退款订单占比: {wide[is_refund].mean():.2%}) print(f支付时长中位数: {wide[pay_duration_min].median():.1f} 分钟)逐段说明退款聚合用groupby加agg字典sum算总退款、count算退款次数、min取首次退款时间这样每个订单只有一行退款记录。merge的howleft保证明细表所有行都保留即使商品维表缺失也不丢单。fillna(0)处理没有退款的订单is_refund标记用于后续筛选。pay_duration_min是核心衍生指标正常订单的支付时长中位数在 5 到 30 分钟之间如果算出来是负数说明时间字段有脏数据需要检查pay_time是否早于order_time。actual_amount的计算假设优惠金额已经分摊到明细行如果原始数据里优惠在订单主表需要先按明细金额比例分摊再计算。3.3 宽表质量校验的四个断言建完宽表不要直接跑分析先过一遍校验。下面四个断言能拦住 80% 的数据问题。# 断言 1实付金额不能为负 assert wide[actual_amount].min() 0, 存在负实付金额检查优惠分摊逻辑 # 断言 2支付时长不能为负 neg_pay wide[wide[pay_duration_min] 0] assert len(neg_pay) 0, f存在 {len(neg_pay)} 条支付时长为负的记录 # 断言 3退款金额不能超过实付金额 over_refund wide[wide[refund_amount] wide[actual_amount]] print(f退款超过实付的记录数: {len(over_refund)}占比: {len(over_refund)/len(wide):.2%}) # 断言 4订单状态枚举值检查 print(订单状态分布:) print(wide[order_status].value_counts(dropnaFalse))断言 1 和 2 是硬性约束不通过必须回去查原始数据。断言 3 允许少量存在因为部分退款可能包含运费补偿但占比超过 1% 就要警惕。断言 4 输出状态分布如果出现NaN说明订单主表有缺失需要确认是数据采集遗漏还是脱敏时删除。我一般会把校验结果写进一个data_quality_report.txt每次更新数据都重新跑一遍对比历史报告看是否有异常波动。4. 用这份数据集跑通三个典型分析场景4.1 场景一复购率计算与 cohort 留存复购率是电商数据集最常被问到的指标但脱敏数据算复购有个坑用户 ID 是哈希后的跨月是否稳定如果脱敏方案是每月重新哈希那跨月复购根本算不了。验证方法是取两个月的用户 ID 集合求交集如果交集为空说明 ID 按月重置。假设 ID 稳定复购率计算如下。# 按用户和月份聚合订单数 wide[order_month] wide[order_time].dt.to_period(M) user_month wide.groupby([user_id, order_month])[order_id].nunique().reset_index() user_month.columns [user_id, order_month, order_cnt] # 计算每个用户的首次购买月份 first_month user_month.groupby(user_id)[order_month].min().reset_index() first_month.columns [user_id, cohort_month] # 关联得到每个用户在首购后第 N 月的购买情况 user_month user_month.merge(first_month, onuser_id) user_month[month_diff] (user_month[order_month] - user_month[cohort_month]).apply(lambda x: x.n) # 留存矩阵行是 cohort 月份列是第 N 月值是留存用户数 retention user_month[user_month[month_diff] 6].pivot_table( indexcohort_month, columnsmonth_diff, valuesuser_id, aggfuncnunique ) print(retention)to_period(M)把时间戳转成月份周期比dt.month更安全因为跨年不会混淆。month_diff用apply(lambda x: x.n)提取周期差值这是 pandas 周期对象的特性。留存矩阵的每一行是一个 cohort第一列是首购人数后续列是第 N 月还在购买的人数。正常 B2C 的次月留存率在 20% 到 35% 之间如果算出来低于 10% 要么是数据时间跨度不够要么是用户 ID 不稳定。4.2 场景二退款率归因到类目和支付时长退款率是电商数据集里最能反映经营质量的指标。脱敏数据虽然看不到退款原因文本但退款原因码通常保留了。下面按类目和支付时长分桶计算退款率。# 按一级类目计算退款率 category_refund wide.groupby(category_l1).agg( order_cnt(order_id, nunique), refund_cnt(is_refund, sum), gmv(actual_amount, sum) ).reset_index() category_refund[refund_rate] category_refund[refund_cnt] / category_refund[order_cnt] category_refund category_refund.sort_values(refund_rate, ascendingFalse) print(category_refund.head(10)) # 支付时长分桶 bins [0, 5, 30, 120, 1440, float(inf)] labels [5分钟内, 5-30分钟, 30分钟-2小时, 2小时-1天, 超过1天] wide[pay_bucket] pd.cut(wide[pay_duration_min], binsbins, labelslabels) pay_refund wide.groupby(pay_bucket, observedTrue).agg( order_cnt(order_id, nunique), refund_rate(is_refund, mean) ).reset_index() print(pay_refund)pd.cut的分桶边界根据业务经验设定5 分钟内是冲动消费退款率通常最低超过 1 天未支付的基本是弃单退款率反而低因为根本没付。真正要关注的是 30 分钟到 2 小时这个区间用户犹豫后下单退款率往往最高。observedTrue参数避免生成空桶。如果某个类目的退款率超过 15%结合refund_cnt看绝对量量大的类目优先排查。4.3 场景三用行为日志做加购转化漏斗行为日志表是这份数据集里最有挖掘价值的部分。典型漏斗是曝光 → 点击 → 加购 → 下单 → 支付。脱敏数据里每个行为都有时间戳可以算每一步的转化率和平均耗时。# 行为日志按用户和商品聚合取每个行为的最早时间 behavior tables[behavior] behavior[event_time] pd.to_datetime(behavior[event_time], errorscoerce) funnel behavior.groupby([user_id, item_id, event_type])[event_time].min().unstack() funnel.columns [ft_{c} for c in funnel.columns] # 计算各步转化 funnel[click_to_cart] (funnel[t_cart].notna()).mean() funnel[cart_to_order] (funnel[t_order].notna() funnel[t_cart].notna()).mean() funnel[order_to_pay] (funnel[t_pay].notna() funnel[t_order].notna()).mean() print(f点击→加购转化率: {funnel[click_to_cart]:.2%}) print(f加购→下单转化率: {funnel[cart_to_order]:.2%}) print(f下单→支付转化率: {funnel[order_to_pay]:.2%})unstack()把event_type从行变成列每个用户商品对一行各行为时间一列。notna()判断是否发生过该行为。正常 B2C 的点击到加购转化率在 5% 到 15%加购到下单在 30% 到 50%下单到支付在 70% 到 90%。如果加购到下单低于 20%说明购物车弃置严重可以进一步分析弃置商品的类目分布和价格带。5. 避坑脱敏电商数据集最常见的五个翻车点5.1 时间字段时区不统一导致漏斗倒挂现象行为日志里点击时间晚于下单时间漏斗算出来转化率超过 100%。原因行为日志用 UTC 时间订单表用北京时间相差 8 小时。解决统一转成同一时区再计算pd.to_datetime后加tz_localize(UTC).tz_convert(Asia/Shanghai)或者直接对订单时间减 8 小时做对齐验证。5.2 商品 ID 在行为日志和订单表里编码方式不同现象行为日志的item_id是 10 位数字订单明细的item_id是 8 位join 后匹配率为 0。原因脱敏时不同表用了不同的哈希规则。解决检查两列的值域是否有重叠如果没有只能放弃跨表关联分别做行为分析和订单分析。如果有部分重叠用astype(str)统一类型后再试。5.3 订单状态枚举值含义不明导致 GMV 口径错误现象把「已取消」订单也算进 GMV销售额虚高。原因状态码是数字没有码表。解决用pay_time是否为空来反推——有支付时间的才是有效订单。更稳妥的做法是统计每个状态码的pay_time空值率和refund_amount均值空值率接近 100% 的状态码就是未支付或已取消。5.4 退款表用订单号关联但一个订单多次退款现象宽表行数比明细表多出 30%。原因退款表一个订单有多条记录直接 merge 导致行数膨胀。解决merge 前先groupby(order_id).agg()聚合到订单粒度或者用merge后drop_duplicates但会丢失退款次数信息推荐前者。5.5 用户 ID 在跨月时重新哈希导致复购率归零现象次月留存率算出来是 0.5%远低于行业水平。原因脱敏方案按月重新生成用户 ID。解决取两个月的数据求用户 ID 交集如果交集占比低于 1% 就确认是重新哈希。这种情况下只能做单月内的分析跨月复购需要换数据集或向数据提供方确认脱敏规则。6. 用 DuckDB 把分析速度提上来以及一个字段映射的后悔药当宽表超过 500 万行Pandas 的groupby会明显变慢。我现在的习惯是把宽表落成 Parquet 文件用 DuckDB 做查询同样的聚合能快 5 到 10 倍。下面这段代码把宽表写入 Parquet 并用 DuckDB 重算类目退款率。import duckdb # 宽表落 Parquet按月份分区 wide[order_month] wide[order_time].dt.strftime(%Y-%m) wide.to_parquet(wide_table.parquet, partition_cols[order_month], indexFalse) # DuckDB 直接查 Parquet不用加载到内存 con duckdb.connect() result con.execute( SELECT category_l1, COUNT(DISTINCT order_id) AS order_cnt, SUM(is_refund) AS refund_cnt, SUM(actual_amount) AS gmv, ROUND(SUM(is_refund) * 1.0 / COUNT(DISTINCT order_id), 4) AS refund_rate FROM wide_table.parquet GROUP BY category_l1 ORDER BY refund_rate DESC LIMIT 10 ).fetchdf() print(result)partition_cols[order_month]让 Parquet 按月份分目录存储DuckDB 查询时如果带月份过滤条件会自动裁剪分区这是提速的关键。COUNT(DISTINCT order_id)在 DuckDB 里比 Pandas 的nunique快很多因为 DuckDB 用了向量化执行。fetchdf()把结果转回 Pandas DataFrame方便后续画图。关于字段映射我最后悔的一次是没有在项目开始就建一个field_mapping.json导致中途改了三次列名所有 notebook 都要重跑。现在的做法是第一步跑profile_columns输出所有列的候选含义人工确认后写进 JSON 文件后续所有代码用df.rename(columnsmapping)统一列名。这个文件也方便交接——别人拿到你的代码看一眼 JSON 就知道每个字段代表什么不用再去翻原始数据。还有一个习惯每次分析结论产出后我会随机抽 10 个订单手工核对从行为日志到订单到退款的完整链路确认数据逻辑自洽。这个动作花不了 10 分钟但能拦住大部分因为 join 或过滤条件写错导致的系统性偏差。数据这行快就是慢慢就是快。希望帮到你。本文还有配套的精品资源点击获取
返回列表