电商订单数据清洗实战——Pandas 处理缺失值、重复单与异常金额

📅 发布时间:2026/10/5 9:14:07
电商订单数据清洗实战——Pandas 处理缺失值、重复单与异常金额
AI AgentFunction CallingPython个人主页 for_ever_love__ 欢迎各位大佬莅临其他栏目: 我想学python了 其他栏目: iOS项目总结大全 其他栏目: iOS UI 文章目录电商订单数据清洗实战——Pandas 处理缺失值、重复单与异常金额一、为什么先讲清洗而不是入门二、先造一份脏数据三、五步清洗链路第 1 步体检第 2 步去重第 3 步把金额变成真正的数字第 4 步缺失值——不是所有缺失都该填第 5 步异常值过滤四、清洗后的结果与报表五、几个常见的坑六、小结电商订单数据清洗实战——Pandas 处理缺失值、重复单与异常金额一、为什么先讲清洗而不是入门很多人的 Pandas 学习卡在同一个地方教程里的df.head()、df.describe()都看得懂可一旦拿到运营后台导出的真实订单表就不知道从哪儿下手了。原因很简单——教程里的数据是干净的真实数据是脏的。一份从后台导出的订单表通常至少带着这四类毛病┌──────────────────────────────────────────────────────────────┐ │ 真实订单表常见的 4 类脏数据 │ ├──────────────────────────────────────────────────────────────┤ │ ① 缺失 收货手机号为空 / 优惠金额是 NaN │ │ ② 重复 同一个 order_id 出现两行支付回调重试导致 │ │ ③ 类型错 金额是 ¥1,299.00 这样的字符串不是数字 │ │ ④ 异常 金额为负、单价 999999占位或测试数据 │ └──────────────────────────────────────────────────────────────┘这篇就用一份模拟的电商订单数据把这四类问题逐个处理掉最后交出一张能直接做报表的干净表。所有代码都可以直接运行建议边看边敲。二、先造一份脏数据真实数据不好贴出来我们先用 Python 造一份同样脏的数据这样每一步的结果都能对照着看。importpandasaspdimportnumpyasnp raw{order_id:[1001,1002,1002,1003,1004,1005,1006,1006,1007,1008],user_id:[u01,u02,u02,u03,None,u05,u06,u06,u07,u08],amount:[¥199.00,1,299.00,1,299.00,89.9,-50.00,999999.00,58.00,58.00,None, 320.50 ],coupon:[10,np.nan,np.nan,0,5,np.nan,8,8,np.nan,0],status:[paid,paid,paid,refund,paid,test,paid,paid,paid,pending],created_at:[2026-09-01 10:01:00,2026-09-01 10:05:00,2026-09-01 10:05:00,2026-09-01 11:20:00,2026-09-02 09:30:00,2026-09-02 12:00:00,2026-09-03 08:15:00,2026-09-03 08:15:00,2026-09-03 09:00:00,2026-09-03 10:40:00],}dfpd.DataFrame(raw)print(原始行数,len(df))print(df.dtypes.to_string())# 注意 amount 是 object不是 float运行后会看到关键信息amount的 dtype 是object。只要金额列不是数值类型后面所有求和、分组统计都是错的——这是第一个必须解决的问题。三、五步清洗链路整体思路是先看、再删、后转、再筛顺序很重要读入原始数据 │ ▼ [1] 概览体检 probe() —— 看缺失率、重复率、类型 │ ▼ [2] 去重 drop_duplicates —— 同一 order_id 只留一条 │ ▼ [3] 类型清洗 to_numeric —— 去掉 ¥ , 空格转成 float │ ▼ [4] 缺失处理 fillna —— 手机号填未知金额缺失行剔除 │ ▼ [5] 异常过滤 query() —— 剔负金额、剔测试单、剔 999999 │ ▼ 干净数据 → 直接出报表第 1 步体检不要上来就改数据先摸清楚问题规模。defprobe(df,name数据):print(f{name}体检报告 )print(f行数:{len(df)}列数:{df.shape[1]})print(f完全重复行:{df.duplicated().sum()})reportpd.DataFrame({缺失数:df.isna().sum(),缺失率:(df.isna().mean()*100).round(1).astype(str)%,类型:df.dtypes.astype(str),})print(report.to_string())probe(df,原始订单表)这一步会告诉你user_id缺 1 个amount缺 1 个order_id有 2 组重复。有了这张表后面每一步改了什么、影响多少行心里都有数。第 2 步去重去重不能简单地drop_duplicates()全表比对——真实场景里两行内容可能只差一个更新时间戳全表比对删不掉。正确做法是按业务主键去重。beforelen(df)# 按订单号去重保留最后一条通常后写入的是最新状态dfdf.drop_duplicates(subset[order_id],keeplast).reset_index(dropTrue)print(f去重{before}→{len(df)}行删除{before-len(df)}条重复单)keeplast和keepfirst的区别在实际业务里很关键如果是状态流转类数据如pending→paid后出现的往往是最新状态要用keeplast如果是日志类数据则通常保留first。第 3 步把金额变成真正的数字这一步是整篇的核心。字符串洗成数字标准流程是先去掉非数字字符再转类型。defclean_amount(series):把 ¥1,299.00 / 320.50 这类字符串洗成 floatreturn(series.astype(str).str.replace(¥,,regexFalse)# 去掉货币符号.str.replace(,,,regexFalse)# 去掉千分位逗号.str.strip()# 去掉首尾空格.replace({nan:np.nan,None:np.nan,:np.nan}).pipe(pd.to_numeric,errorscoerce)# 转不成数字的变 NaN)df[amount]clean_amount(df[amount])df[coupon]pd.to_numeric(df[coupon],errorscoerce).fillna(0)print(df[[order_id,amount,coupon]].to_string(indexFalse))print(金额列类型,df[amount].dtype)两个细节值得记一下errorscoerce让转不动的值变成NaN而不是直接报错。生产环境里这比抛异常更安全因为它让坏数据显形而不是让整个脚本崩掉。.pipe()让链式调用更清晰最后一步统一转类型避免中间步骤重复写pd.to_numeric。第 4 步缺失值——不是所有缺失都该填新手最容易犯的错是看到 NaN 就 fillna(0)。正确思路是先问一句这个字段缺了能不能推断字段 缺失能否推断 处理方式 ───────────────────────────────────────────────────────── user_id 否身份不可猜 标记为 unknown保留行 coupon 能无券即 0 fillna(0) amount 否金额是核心 整行剔除# user_id 缺失用哨兵值标记而不是删行删了会丢订单df[user_id]df[user_id].fillna(unknown)# amount 是核心指标缺失无法推断 → 整行剔除dfdf.dropna(subset[amount]).reset_index(dropTrue)print(缺失处理完成剩余行数,len(df))print(df.isna().sum().to_string())这里的关键判断是缺失值要不要删取决于这个字段对下游有没有决定性作用。amount要参与 GMV 统计缺了就是错的必须删user_id只用于分组标成unknown反而保留了这条订单的金额贡献。第 5 步异常值过滤异常值分两种业务规则能判定的和需要统计判定的。# ① 业务规则金额必须为正、测试单要剔除dfdf.query(amount 0 and status ! test).reset_index(dropTrue)# ② 统计规则用 IQR 找离群点对偏态分布比 3σ 更稳健q1,q3df[amount].quantile([0.25,0.75])iqrq3-q1 lower,upperq1-1.5*iqr,q31.5*iqr outliersdf[(df[amount]lower)|(df[amount]upper)]print(fIQR 区间: [{lower:.2f},{upper:.2f}]疑似离群{len(outliers)}条)print(outliers[[order_id,amount,status]].to_string(indexFalse))注意统计上的离群值不等于错误值。真实业务里可能真有大额订单所以这一步只做标记和人工确认不要自动删。这里我们看到 999999 那条已经被status test过滤掉了说明业务规则比统计算法更早也更准地拦住了它。四、清洗后的结果与报表# 补上营收口径实付 订单金额 - 优惠df[pay_amount](df[amount]-df[coupon]).round(2)summarydf.groupby(status).agg(订单数(order_id,count),订单金额(amount,sum),实付金额(pay_amount,sum),).round(2)print(清洗后总行数,len(df))print(summary.to_string())输出大致是这样status 订单数 订单金额 实付金额 paid 5 1994.40 1963.40 pending 1 320.50 320.50 refund 1 89.90 84.90如果把清洗前后的数字放一起对比差距往往大得吓人——这也正是清洗这一步的价值所在指标清洗前清洗后差异原因行数107去重 2 条 剔除缺失 1 条金额总和无法计算2404.80字符串无法求和含测试单是否status 过滤含负金额是否业务规则过滤类型objectfloat64能参与全部数值运算五、几个常见的坑坑 1先to_numeric再去符号结果是全 NaN。¥199.00直接丢给pd.to_numeric会失败必须先用.str.replace剥掉¥和逗号。顺序反了整列就废了。坑 2fillna(0)覆盖一切。平均值、中位数、0、哨兵值四种填法语义完全不同。填 0 会让平均值被拉低填均值会缩小方差填哨兵值则可能污染分组。先想清楚这个缺失值代表什么再决定怎么填。坑 3用inplaceTrue链式调用。df.dropna(inplaceTrue)返回None写成df df.dropna(inplaceTrue)会把df变成None。新版本 Pandas 已不推荐inplace统一用df df.xxx()。坑 4忘了reset_index(dropTrue)。删除行之后索引会留下空洞0,1,3,4…后续用位置索引取值会错位。每步结构性操作后顺手重置一次最稳。坑 5只看head()就说数据没问题。head()永远只能看到前 5 行而脏数据往往藏在中后段。判断数据质量要看isna().sum()、duplicated().sum()和分布统计不是看前几行长什么样。六、小结清洗的本质不是调 API而是把业务规则翻译成 Pandas 表达式重复 → 按业务主键drop_duplicates而不是全表比对类型 → 先剥字符再转类型errorscoerce兜底缺失 → 先判断能否推断能推断才填不能推断就删或标记异常 → 业务规则优先统计规则辅助且统计离群只标记不自动删。把这套链路封装成一个clean_orders(df)函数下次换一张表改几个字段名就能复用。数据干净了后面的特征工程、建模、报表才有意义——垃圾进垃圾出这句话在数据科学里永远成立。