做电商数据这几年,我最大的感受是:真正让人头秃的不是报表怎么写、模型怎么调,而是最底层的订单数据根本没法直接用。你以为导出来的订单明细是“事实”,实际上里面全是坑——重复行、空值、格式错乱、金额对不上、状态自相矛盾,随便一个都能让汇总结果偏得离谱。这篇博客就把我日常处理电商订单数据的心法梳理一遍,重点讲怎么用pandas把脏数据一步一步清洗成能真实反映业务事实的干净数据。整套流程不仅适合数据分析师、运营同学,也适合刚接触数据清洗的初学者照着上手。
1. 电商订单数据的“脏”到底脏在哪
1.1 从一份真实导出文件说起
先还原一个最常见的场景:某电商平台后台导出订单明细,Excel打开一看,几万行数据,表头倒是齐全,但仔细看就发现问题了。订单号有的带前缀、有的不带,有的是文本格式、有的被Excel自动转成了科学计数法;时间字段更乱,有些是“2024-08-15 14:22:31”,有些是“2024/8/15”,还有几个干脆是纯数字的Excel序列值;金额列里混着“¥298.00”这种带货币符号的文本;状态字段一会儿是“已完成”、一会儿是“COMPLETED”、一会儿又是“完成”带个空格。
这还算好的。往下翻还能看到同一笔订单在表里出现两次,但其中一行金额是0;收货地址里有“省 市 区”被拆得乱七八糟;部分订单的SKU名称带着肉眼可见的乱码;甚至还有几行的“订单金额”和“商品金额+运费-优惠”完全对不上号。这种表如果直接拿去跑汇总,出来的数字没人敢信。
1.2 脏数据的三类典型形态
我习惯把电商订单里的脏数据归成三大类。
第一类是结构性问题,比如重复记录、列名不统一、数据错位。错位这个最坑,有时候是因为手工合并表格时某一行少了几个单元格,后面的列整体前移,订单号下面填的是用户ID,用户ID下面填的是收货人,整列数据都是歪的。
第二类是内容性问题,包括格式不统一、单位不一致、大小写混用、编码错误。这类问题不影响“行数”看起来正常,但一用groupby或者条件筛选,就有大量数据被漏掉。比如按“已完成”筛选时,漏掉了“COMPLETED”和“完成 ”,统计结果直接少了一截。
第三类是逻辑性问题,这是最隐蔽的。字段单独看都不为空、格式也都对,但放在一起就是矛盾。订单金额不等于明细之和、下单时间晚于付款时间、退款金额大于实付金额,这种数据靠单个字段校验根本发现不了,必须做交叉验证。
1.3 脏数据对业务判断的真实伤害
有人觉得“脏数据也就差个5%”,但电商订单数据的问题恰恰在于脏数据往往不是随机分布的。系统异常、漏单、重复支付这些情况经常集中在某个渠道、某个时段、某类商品上,清洗不干净会让偏差呈现在特定维度里,比如某个渠道的转化率被重复订单虚高,某个品类的退款率因为状态字段不一致被低估。
我之前遇到过一档子事——运营部门上线了一个单品活动,想复盘活动期间的销售额。结果导出的订单里有大量测试订单混在里面,订单备注写着“测试单,勿发”,但金额真实、状态也正常,直接汇总下来销售额虚高了20%。这种情况下,数据清洗不光是技术问题,更是业务底线问题。清洗的目的不是让数据“看起来干净”,而是让数据能真实还原业务里发生了什么。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 清洗方案设计:不先想清楚就动手等于白干
2.1 清洗之前先回答四个问题
拿到原始数据后,我习惯先不急着写代码,而是先逼自己回答四个问题。
第一个问题:这份数据要用来回答什么业务问题?做GMV汇总、做用户购买行为分析、做商品销售排行,这三种用途对清洗的要求完全不同。如果只是看总量趋势,个别异常值可以直接剔除;如果要做用户维度分析,就必须保证一个用户ID对应真实唯一的用户,不能把同一个人的不同账号合并了。
第二个问题:哪些字段是这次分析需要的?很多订单表有几十列,但实际用到的可能就十列。无关列可以先留着不处理,但也不用花精力去清洗。最忌讳的是拿到表就把所有字段都清洗一遍,时间全耗在无关紧要的“收货地址补全”上,核心的金额、时间、状态却没做交叉验证。
第三个问题:你手上有没有可参照的“真相”?比如订单主表的金额和支付流水表是否对得上,订单状态和物流状态是否自洽。清洗不是闭门造车,很多时候要有外部参照才能判断哪条数据是错的。
第四个问题:清洗后的数据要落到哪里?是输出一份带标记的明细表、入库到数仓,还是直接做成汇总报表?这决定了你清洗过程中需不需要保留原始值、要不要加清洗标记列。我一般会保留原始值,新增清洗后的列,而不是直接覆盖,这样就可以随时追溯。
2.2 为什么我最终选了pandas这套方案
处理电商订单数据,技术栈五花八门,有人用Excel函数硬扛,有人用SQL在数据库里做,有人用Python写脚本。我自己的选择是pandas,原因其实很朴素。
Excel适合快速看一眼数据长什么样,但真处理几万行订单、做跨字段逻辑校验的时候,Excel的公式就明显力不从心了,操作步骤没法复现,换个批次数据又得点一遍。
SQL在数据库里清洗确实规范,但很多电商订单原始数据是Excel文件或CSV导出的,不是所有场景都能先落库再处理。而且SQL做字符串清洗和正则替换比较绕,一些复杂逻辑写着也费劲。
pandas的真正优势在于:加载CSV/Excel方便,对脏数据的容忍度高(可以先把所有列读成字符串再处理),处理过程是脚本化的、可复现的,换一批数据改个路径就能跑,而且数据量在几十万行以内时性能完全没问题。对大多数电商团队的订单数据规模来说,pandas+Jupyter Notebook就是我心目中性价比最高的方案。
2.3 清洗流程怎么排才不容易返工
清洗流程我有一个固定的节奏,按这个顺序走基本不会乱,也不容易把数据改坏。
第一步,摸清家底。把数据读进来,看形状、看列名、看每列的非空数量、看类型和几个样本值。这一步不修改任何数据,只是搞清楚“有什么、缺什么、怪在哪”。
第二步,处理结构问题。先做列名统一,再处理重复行和多行同单的问题。这一步解决的是“表本身是不是一张规整的表”。
第三步,处理内容问题。格式统一、去空格、类型转换、状态字段标准化。这一步解决的是“每个单元格的值是否规范”。
第四步,做逻辑校验。金额勾稽、时间顺序、状态自洽、跨字段对比。这一步解决的是“字段之间的关系是否成立”。
第五步,输出并验证。把清洗后的数据落盘,同时生成一份数据质量报告,记录每个环节处理了多少行、哪些规则命中了哪些数据,方便后面回溯。
这个顺序有讲究——必须先保证表结构和粒度是稳定的,再做字段标准化。否则同一笔订单一会儿是一行一会儿是两行,你前面做的格式处理可能在去重后就白费了。
3. 核心实战:用pandas六步把订单数据洗得干干净净
3.1 第一步:读取数据,先搞清楚家底
整个清洗过程从读取数据开始。我通常用pd.read_csv或pd.read_excel加载原始文件,但有几个参数是必须用上的。
python复制import pandas as pd
df = pd.read_excel(
"电商订单导出_20240815.xlsx",
dtype=str, # 先全部按文本读入,避免类型自动转换
keep_default_na=False, # 不把空字符串自动转为NaN
na_values=["", "#N/A", "NULL", "null", "无"]
)
这里dtype=str是我强烈建议的做法——先全按文本读,后面再统一转类型。原因很实际,Excel读进来的时候,那些带前导零的订单号、身份证号、手机号会被自动转成数值,精度丢失,后面怎么都救不回来。如果一开始全按字符串读,至少保留了原始信息,后续想转数值再手动转。
读进来之后,第一件事是看整体情况。
python复制print(df.shape) # 行数和列数
print(df.columns.tolist()) # 全部列名
print(df.dtypes) # 每列类型
print(df.isna().sum()) # 每列缺失情况
print(df.head(10).T) # 前10行,转置后方便一条条看
这里有两个细节值得注意:一是Excel里某些单元格是“真的空”,某些是空格字符串,某些是“NULL”文本,keep_default_na=False配合na_values可以把它们统一成NaN,后面统一处理;二是先不要用dropna之类的函数删数据,你还没搞清楚“为什么空”,就直接删,很可能把有业务含义的缺失删掉了。
3.2 第二步:列名统一和字段裁剪
读进来的列名往往五花八门,中文的、英文的、带空格的、带括号的都有。我会先做一个字段映射表,把列名统一成规范格式,并且只保留这次分析需要的列。
python复制# 原始列名和统一列名的映射
column_map = {
"订单编号": "order_id",
"订单号": "order_id",
"下单时间": "order_time",
"订单创建时间": "order_time",
"商品金额": "product_amount",
"SKU金额": "product_amount",
"运费": "freight",
"优惠金额": "discount",
"实付金额": "paid_amount",
"订单总金额": "total_amount",
"支付状态": "pay_status",
"订单状态": "order_status",
}
# 只保留需要的列,并统一重命名
keep_cols = [c for c in df.columns if c in column_map]
df = df[keep_cols].rename(columns=column_map)
这个映射表看着简单,但它的作用不是省打字,而是让清洗脚本对“不同批次导出”的表格有更强的容忍度。同一个平台不同时间段导出的表,列名可能改了,但语义没变。有了映射表,下次换一批数据,只需要调整映射关系就行了。
还有一点:列名统一里我会额外处理order_id的前后空格。很多时候列名看着没问题,其实带着不可见字符,比如UTF-8的BOM头。我一般直接df.columns = [c.strip() for c in df.columns]先把列名全部清洗一遍,再做映射。
3.3 第三步:重复订单——哪些该删,哪些不能删
重复数据是订单表里最普遍的脏数据问题。但重复也分很多种,处理方式完全不同,千万别一看到重复就drop_duplicates()一把梭。
第一种是“完全重复行”:整行所有字段的值都一样,没有其他信息差异,基本可以确定是导出过程造成的冗余,直接删除,保留一行即可。
第二种是“订单号重复但内容不同”。这种必须单独看——可能是同一笔订单被修改过(比如先下单后取消),系统里保留了多个快照;可能是同一个订单号在不同状态节点重复记录;也可能是系统BUG导致出现了两行相似但金额不同的记录。直接删是危险的,需要判断你的分析按什么粒度来。如果是分析“订单最终状态”,那就按订单号取最新一条;如果是分析“订单全生命周期”,那就都得留着。
第三种是“不同订单号但业务上是一笔订单”。比如拆单、合并支付、父订单和子订单关系。这种光看重复检测是不行的,得结合订单号规则、关联字段来判断。
实际操作里,我第一步会先按所有列去重,再按订单号判断“同一订单多行”的情况。
python复制# 第一步:完全重复行,直接删除
df = df.drop_duplicates()
# 第二步:检查每个订单号出现次数
dup_stats = df.groupby("order_id").size().reset_index(name="count")
multi_orders = dup_stats[dup_stats["count"] > 1]
print(f"同一订单号出现多行的数量: {len(multi_orders)}")
这里我建议不要把多行数据直接删掉,而是先打印出来人工看一眼。我在多次实操中的经验是:同一订单号多行记录里的“下单时间”分布有规律,如果时间完全一致,多半是重复导出;如果时间有先后,很可能是订单状态变更的历史记录。这两种情况的处理逻辑完全不同,前者保留一行,后者要具体看,有时要保留状态为“最终态”的那一行,有时要全部保留用于过程分析。
python复制# 以订单时间排序后,同一订单只保留最后一条记录
df = df.sort_values(["order_id", "order_time"], ascending=[True, True])
df = df.drop_duplicates(subset=["order_id"], keep="last")
这个策略只适用于“分析最终订单状态”的场景。如果你的场景是分析用户下单转化漏斗,那中间的每个状态节点都是有效数据,就不能这么删了。判断标准永远是:你的分析口径要落到什么粒度。
3.4 第四步:缺失值处理——先分场景再填,别只会填零
缺失值在订单表里很常见,但我见过太多人不管三七二十一先fillna(0),结果把大量“没有优惠”和“数据缺失”混为一谈,后面做价格分析时全是错的。
订单表里常见的缺失场景有几种。
一是“该有但缺失”的字段,比如实付金额为空。这种数据如果无法从其他字段推算,通常只能标记出来,不能随便填一个0,填0会虚增退款率、虚降客单价。
二是“不该有所以空”的字段,比如未发货订单没有物流单号、未支付订单没有支付时间。这种情况的缺失本身就是业务信息,不需要填充,反而可以派生一个“是否支付”的布尔特征。
三是“填了可能误导”的字段。比如优惠金额为空,到底是“没优惠”还是“有优惠但没记录”?在无法确认的情况下,我把空值统一填充为0,但会加一个标记列discount_missing,后面分析时如果发现异常再检查。
实操里我的处理套路是:先统计缺失率,再按字段语义分场景处理。缺失率超过80%的字段,这次分析基本可以放弃它的颗粒度;缺失率低但关键性强的字段,优先考虑用同订单其他字段推算;推算不了的,就保留空值并加标记列,绝不强行填数。
python复制# 统计缺失率
miss_rate = df.isna().mean().sort_values(ascending=False)
print(miss_rate)
# 针对“优惠金额”这种默认“没有优惠”的字段,填0
df["discount"] = df["discount"].fillna(0)
# 针对“支付时间”这种“未支付就不该有”的字段,不填,派生布尔列
df["is_paid"] = df["pay_time"].notna().astype(int)
这里最关键的一个原则是:缺失值要么是你明确知道怎么填的,要么就得保留原样并打标记,绝对不能为了“数据完整”而编造一个数值出来。 数据清洗做的不是美化数据,而是忠实反映业务事实。
3.5 第五步:格式统一和异常值识别
格式问题表面上五花八门,本质上就是几种:空格和不可见字符、大小写不统一、全角半角混用、时间格式不一致、数值列混入了文本符号。
我处理的第一步是去空格和统一大小写。
python复制# 对所有字符串列去掉首尾空格
str_cols = df.select_dtypes(include=["object"]).columns
df[str_cols] = df[str_cols].apply(lambda x: x.str.strip())
# 状态字段统一成小写再映射
df["order_status_std"] = df["order_status"].str.lower().map({
"已完成": "completed",
"complete": "completed",
"completed": "completed",
"已取消": "cancelled",
"cancel": "cancelled",
"cancelled": "cancelled",
"待支付": "pending",
"pending": "pending",
"已支付": "paid",
"paid": "paid",
})
这个映射表的方法,比一堆df.loc条件判断要好维护得多。哪天平台又改了个状态名称,只需要往映射表里加一行就行。多状态值之间用“同义词归一”的思路处理,比一个个replace更可靠。
时间格式统一是我每次都会遇到的重灾区。我的做法是用pd.to_datetime,但它有几个坑必须先说明。
python复制df["order_time_std"] = pd.to_datetime(
df["order_time"],
format="mixed", # pandas 2.0+支持自动识别混合格式
errors="coerce" # 无法解析的置为NaT,后面统一排查
)
format="mixed"能处理“2024-08-15 14:22:31”和“2024/8/15”混在一起的情况。errors="coerce"会把解析不了的置为NaT,然后你就能打印出来看看哪些格式还没覆盖到。这里有一个经验:平台导出文件的日期问题,很多时候是Excel把一些行自动转成了日期序列值(比如45354这种),这种纯数字时间pd.to_datetime是解析不了的。需要先判断哪些是非字符串的数字,用pd.to_datetime(45354, unit="D", origin="1899-12-30")转回来。Excel的日期序列是从1900年1月1日算起的,但里面有个著名的“1900年闰年bug”,所以用1899-12-30作为origin才是正确算法。
金额字段的清洗要小心,因为金额列里会出现“¥298.00”“1,299.00”“299元”这类带符号的文本。我的做法是先把所有非数字字符替换掉,再转成浮点数,但一定要先确认这一列没有其他乱七八糟的符号。
python复制import re
def clean_amount(value):
if pd.isna(value):
return np.nan
# 去掉货币符号、逗号、中文“元”等
s = re.sub(r"[¥¥,,\s元]", "", str(value))
try:
return float(s)
except ValueError:
return np.nan # 转不了的置为NaN,统一排查
df["paid_amount_clean"] = df["paid_amount"].apply(clean_amount)
异常值识别上,除了数值范围的上下界判断(金额不能为负、折扣不能大于商品金额),我更关注的是“条件异常”。比如:订单状态是“未支付”但支付时间有值;状态是“已取消”但实付金额大于0。这些逻辑矛盾单看数值是正常的,但和业务规则一对比就露馅了。这类问题的排查思路放到下一节专门讲。
3.6 第六步:逻辑校验——让数据真正能反映业务事实
格式清洗完毕之后,整个清洗流程里最重要的一步是逻辑校验。这一步的核心思路是:数据中的每个字段不应该是孤立的,它们之间有业务上的勾稽关系,校验这些关系就能发现隐藏的脏数据。
订单表中最经典的勾稽关系是:商品金额 + 运费 - 优惠金额 = 实付金额。平台导出的字段叫法可能各不相同,但这个等式在任何电商平台都是成立的,除非有异常。我的做法是构造一个“金额差异列”,然后看差异的分布,用数据本身来找出有问题的记录。
python复制df["calc_total"] = (
df["product_amount"].fillna(0)
+ df["freight"].fillna(0)
- df["discount"].fillna(0)
)
df["amount_diff"] = df["calc_total"] - df["paid_amount_clean"]
# 差异绝对值大于1元的记录就是可疑数据
suspect_amount = df[df["amount_diff"].abs() > 1]
print(f"金额不匹配的记录数: {len(suspect_amount)}")
这里阈值为什么取1元?因为浮点数计算本身可能有几分钱的误差,而且平台导出的金额字段往往是分和元的精度混合,取1元做阈值能过滤掉正常误差,同时抓出真正对不上的大问题。但阈值怎么定应该根据业务场景来——如果你要精确核账,阈值就提到0.01元;如果只是评估数据整体质量,1元就够了。
除了金额勾稽,时间顺序校验也很重要。正常情况下“下单时间 <= 支付时间 <= 发货时间 <= 签收时间”。如果出现支付时间早于下单时间,基本可以断定有时间字段解析错误或者数据源拼接错位了。
python复制time_violations = df[
(df["pay_time"].notna())
& (df["order_time"].notna())
& (df["pay_time"] < df["order_time"])
]
print(f"支付时间早于下单时间的记录数: {len(time_violations)}")
状态一致性问题上,我的经验是:与其一个一个状态去写判断逻辑,不如先做一张“状态-期望行为”对照表,然后批量校验。比如“已取消”的订单,期望实付金额为0或空;“已支付”的订单,期望支付时间不为空。把规则当作数据表维护,扩展性好,后面新增规则也不用改代码框架。
到这里,经过六步清洗,数据基本具备了“能反映业务事实”的基础。但清洗到这一步还不算完,还得验证清洗质量,看看到底洗掉多少、改了什么,以及检查清洗本身有没有误伤。
4. 清洗质量验证与常见问题排查
4.1 清洗前后对比:数据质量报告怎么做
每次清洗完,我都会自动生成一份数据质量报告,一方面给自己留底,另一方面也给后续接手数据的人一个交代。报告内容通常包含几个固定指标。
第一个是行数变化。原始数据多少行,清洗后多少行,删除了多少行,删除原因分布是什么。这里要特别留意“重复订单删除”占总删除行的比例,如果这个比例异常高,可能不是重复数据的问题,而是原始数据导出时就做了多级汇总,需要重新看数据源。
第二个是空值率变化。清洗前哪些列空值率是多少,清洗后空值率变化到多少。有些字段(如支付时间)空值率下降是因为格式清洗从文本“null”识别成了真正的空值;有些字段空值率上升可能是errors="coerce"把解析不了的时间置成了NaT,这种需要人工确认。
第三个是异常规则命中情况。金额不匹配多少条,时间逻辑矛盾多少条,状态异常多少条。这些数字本身就是数据质量的“体检指标”,如果命中率持续偏高,说明上游数据源的问题比想象中严重。
python复制quality_report = {
"raw_rows": raw_rows,
"clean_rows": len(df),
"dup_rows_removed": dup_count,
"missing_before": miss_before,
"missing_after": df.isna().sum().to_dict(),
"amount_mismatch": len(suspect_amount),
"time_violations": len(time_violations),
}
print("清洗质量报告:", quality_report)
4.2 不同订单来源的数据差异怎么对齐
电商订单的来源往往不止一个:有主站App的、有小程序商城的、有线下门店POS机的、有第三方渠道下单的,每个渠道导出的订单字段默认值都不一样。小程序商城可能没有“线下门店ID”,POS机订单可能没有“优惠券ID”。同一渠道表结构不同,甚至同一渠道不同时间的导出都存在差异。
这种情况我常用的方案是“字段对齐层”——在合并之前,先做一层字段映射和补全,把所有来源的订单统一到同一个“标准字段集”上,缺失的字段填一个统一的占位值并加列标记来源。等所有数据都进到同一张“标准订单表”后再做清洗,效率高很多。
实际操作中还要注意ID的命名冲突问题。不同渠道的订单号可能都是数字自增,从1开始的,合在一起后会撞号。所以合并前一定要给每行数据加一个“来源渠道”列,并且把订单ID改成“渠道前缀+原订单号”的复合ID,否则后续去重和关联都会出问题。
4.3 订单数据清洗时容易踩的五个坑
踩过的坑多了,就总结出几条自己用血的教训换来的经验。
第一个坑:清洗和汇总混在一起。有人在写pandas的时候,边清洗边做groupby统计,结果清洗逻辑和业务逻辑纠缠在一起,出了错根本分不清是清洗的问题还是统计的问题。正确做法是清洗阶段只输出“干净的明细表”,统计汇总必须放在新的代码块里做。
第二个坑:直接覆盖原始列。清洗一时爽,复盘火葬场。当你发现某一步清洗逻辑写错的时候,原始值已经被覆盖了,想追回去根本不现实。我的习惯是所有清洗结果新增一列,比如order_time_std、paid_amount_clean,原始列原封不动保留。
第三个坑:掉了重复数据的“业务重复”场景。比如用户下了一单,然后又取消,又重新下一单,这三笔订单订单号不同、金额相同,但都是真实的业务记录,不是重复数据。如果机械地按“订单号+金额”去重,就会把真实订单误删。所以去重前一定要想清楚“重复”在这份数据里的定义是什么。
第四个坑:空值的业务含义没有区分就统一填充。把“未发货”的物流单号填成“无”,和把“未支付”的支付时间填成1970-01-01,性质完全不同。填一个虚构的值比留空更危险,因为后面分析的人不会知道你填的值是编出来的。
第五个坑:编码问题导致中文乱码。用pd.read_csv读文件时指定encoding="utf-8"或“gbk”,具体取决于文件来源。Excel另存的CSV经常是GBK编码,Linux服务器导出的往往是UTF-8。读错编码会出现中文乱码,最坑的是乱码不报错,数据量不大时肉眼很难发现。
4.4 清洗后的数据如何验证“真的干净了”
清洗结束之后,千万别急着交付,我一般会做几轮验证。
第一轮是抽样核验。从清洗后的数据里随机抽30~50条记录,回到原始数据里人工比对。重点看金额、时间、状态这几个核心字段,确认清洗逻辑没有把对的数据改成错的。
第二轮是总量核验。如果之前从数据库或报表系统里能查到同期的订单总量、GMV总量,就拿清洗后的数据汇总对比一下。总量相近、差异有合理解释(比如剔除了测试单)时,数据基本可信;如果差异巨大,就要回头查清洗规则。
第三轮是下游验证。拿清洗后的数据跑一遍你最终要做的分析,比如按渠道看销售额、按商品看销量,确认结果和业务认知是一致的。如果某个渠道的销售额突然比平时波动明显,可能并不代表业务变化,而是数据清洗时出了偏差。
第四轮是规则回归。把清洗脚本保存下来,隔一段时间或换一批数据源时重新跑一遍,对比前后两轮的清洗报告。如果某些规则命中的数量突然暴涨,说明上游数据源发生变化,需要重新审视清洗规则。
我个人的习惯是:清洗脚本本身也要做“版本管理”。 每次调整清洗逻辑后,都保存一个新的脚本版本,同时留一份当次运行的清洗报告。这样一个月后如果有人问“3月份的数据为什么和4月份的口径不一样”,你能直接找到当时是怎么处理的,而不是凭记忆回忆。
4.5 一份可以照着改的完整清洗脚本
最后给出一份我日常使用的清洗脚本骨架,所有处理逻辑都按模块组织,换一批数据只需要改路径和映射表。
python复制import pandas as pd
import numpy as np
import re
# ========== 配置区 ==========
INPUT_FILE = "电商订单导出_20240815.xlsx"
OUTPUT_FILE = "电商订单_clean_20240815.csv"
COLUMN_MAP = {
"订单编号": "order_id",
"下单时间": "order_time",
"支付时间": "pay_time",
"商品金额": "product_amount",
"运费": "freight",
"优惠金额": "discount",
"实付金额": "paid_amount",
"订单状态": "order_status",
}
STATUS_MAP = {
"已完成": "completed", "complete": "completed", "completed": "completed",
"已取消": "cancelled", "cancel": "cancelled", "cancelled": "cancelled",
"待支付": "pending", "pending": "pending",
"已支付": "paid", "paid": "paid",
}
# ========== 读取 ==========
df = pd.read_excel(INPUT_FILE, dtype=str, keep_default_na=False)
df.columns = [c.strip() for c in df.columns]
# ========== 列名统一 ==========
keep_cols = [c for c in df.columns if c in COLUMN_MAP]
df = df[keep_cols].rename(columns=COLUMN_MAP)
# ========== 去重 ==========
df = df.drop_duplicates()
df = df.sort_values(["order_id", "order_time"], ascending=[True, True])
df = df.drop_duplicates(subset=["order_id"], keep="last")
# ========== 格式标准化 ==========
df["order_status_std"] = df["order_status"].str.lower().map(STATUS_MAP)
df["order_time_std"] = pd.to_datetime(df["order_time"], errors="coerce")
df["pay_time_std"] = pd.to_datetime(df["pay_time"], errors="coerce")
# 金额清洗
def clean_amount(x):
if pd.isna(x) or str(x).strip() == "":
return np.nan
s = re.sub(r"[¥¥,,\s元]", "", str(x))
try:
return float(s)
except ValueError:
return np.nan
df["product_amount_clean"] = df["product_amount"].apply(clean_amount)
df["freight_clean"] = df["freight"].apply(clean_amount)
df["discount_clean"] = df["discount"].apply(clean_amount)
df["paid_amount_clean"] = df["paid_amount"].apply(clean_amount)
# 金额勾稽校验
df["calc_total"] = (
df["product_amount_clean"].fillna(0)
+ df["freight_clean"].fillna(0)
- df["discount_clean"].fillna(0)
)
df["amount_diff"] = df["calc_total"] - df["paid_amount_clean"]
df["is_amount_ok"] = df["amount_diff"].abs() <= 1
# 时间顺序校验
df["is_time_ok"] = ~(
df["pay_time_std"].notna()
& df["order_time_std"].notna()
& (df["pay_time_std"] < df["order_time_std"])
)
# ========== 输出标注 ==========
df["is_test_order"] = df["order_id"].str.contains("test|测试", case=False, na=False)
df_clean = df[df["is_test_order"] == False].copy()
df_clean.to_csv(OUTPUT_FILE, index=False, encoding="utf-8-sig")
print(f"清洗完成,输出: {OUTPUT_FILE}")
print(f"原始行数: {len(df)},最终行数: {len(df_clean)}")
这段脚本里encoding="utf-8-sig"是为了让Excel能正确识别UTF-8的CSV文件,不加-sig的话Excel打开CSV会中文乱码。这也是从实际工作里踩坑踩出来的细节。
5. 从“能出数”到“敢用数”,数据清洗真正解决的问题
数据清洗这件事,表面上是在处理数据,本质上是建立信任。分析师敢不敢用一张表做决策,运营敢不敢拿一个数字向管理层汇报,取决于数据是不是经得起追问。清洗的目的不是把数据变“好看”,而是让每一行数据都能经得起“这笔订单真的发生过吗”“这个金额真的收进来了吗”这样的追问。
我处理过的项目里,数据清洗的投入产出比往往是最被低估的。前期花两三天把数据基础打好,后面做任何分析和报表都能少踩无数的坑;反过来,如果基础数据没打好,省下的那一两天清洗时间,会在后续分析里以十倍百倍的返工成本找回来。
如果你刚接触这块,我的建议很简单:不要追求一次写出完美的清洗脚本,也不用把pandas所有函数都学会再动手。拿到一份真实的脏数据,先把今天文章里说的六步流程走一遍,每一步都停下来看一眼中间结果——去重后行数少了多少,时间解析失败了多少条,金额不匹配的又有多少条。等你亲眼看到这些数字的变化,你就真正理解了数据清洗是在干什么。剩下的,无非是遇到一个新问题,就多补一条新规则而已。
