
多部门、多门店各交一张月度报表,最后要合成一张总表——这是所有做数据的人最常见的起点。难点从来不是"合",而是合完之后没人敢为这张表签字:同一个字段在 A 表叫「金额」、B 表叫「总价」、C 表叫 amount;日期有 2026/08/01 也有 20260801;单号重复、金额对不上、客户名空白……你不知道里面藏着多少坑,只能祈祷。
在 WorkBuddy 里我让 AI 直接动手验证了这个场景:先用程序合成 18 个 xlsx 分表(26,954 行,故意注入了重复、缺失、勾稽错误、非法值、跨表冲突五类脏数据),再写一条治理式合并流水线跑真实对比。所有数字都是实测出来的,可复现。
pd.concat 只解决"拼起来",不解决"可信"根因分析只有一句话:pd.concat 是物理拼接,它默认所有表的列语义一致。而现实是表头漂移、格式漂移、口径漂移三件事必然发生。所以对照组(直接 concat)跑完的结果是:23 列、40 万个空单元格、0 条校验、0 个问题——它不会报错,这才是最大的错。
指标 | pd.concat 一把梭 | 治理式合并 + 校验 |
|---|---|---|
合并后列数 | 23 列(表头各写各的) | 8 列 + 来源标记 |
空单元格 | 404,642 个 | 业务必填列全部核对 |
校验规则 | 0 条 | 6 条 |
发现问题 | 0 处(静默通过) | 1,277 处 |
去重后行数 | 26,954 | 26,792(剔除 162 行完全重复) |
干净可用行数 | — | 26,216(隔离 576 行,隔离率 2.15%) |
耗时 | 2.82 s | 3.14 s |

最扎眼的是金额:全部行合计 ¥252,341,849.90,干净行合计 ¥246,802,692.06,差额 ¥5,539,157.84。也就是说,如果不做校验直接把总表交给下游,大约 553 万元的错数会一路流进报表。

核心逻辑五十行以内:列名归一 → 数值/日期清洗 → 六条规则逐条命中 → 命中行隔离落盘。
import glob, re, unicodedata
import pandas as pd
from datetime import datetime
HEADER_MAP = {"订单编号":"order_id","订单号":"order_id","order_id":"order_id",
"下单日期":"order_date","日期":"order_date","date":"order_date",
"商品编码":"sku","SKU":"sku","货号":"sku",
"数量":"qty","件数":"qty","qty":"qty",
"单价":"price","price":"price",
"金额":"amount","总价":"amount","amount":"amount","total":"amount",
"客户名称":"customer","客户":"customer",
"订单状态":"status","状态":"status"}
DATE_FMTS = ["%Y-%m-%d","%Y/%m/%d","%Y%m%d","%d-%m-%Y"]
def norm(s):
s = unicodedata.normalize("NFKC", str(s)).strip().lower()
return HEADER_MAP.get(s, s)
frames = []
for f in glob.glob("./raw/*.xlsx"):
df = pd.read_excel(f, dtype=object)
df.columns = [norm(c) for c in df.columns]
df["__src"] = f
frames.append(df)
m = pd.concat(frames, ignore_index=True, sort=False)
def num(v):
s = str(v).replace("¥","").replace(",","").strip()
try: return round(float(s), 2)
except Exception: return None
def pdate(v):
for f in DATE_FMTS:
try: return datetime.strptime(str(v).strip(), f)
except Exception: pass
return None
m["amount"] = m["amount"].map(num)
m["price"] = m["price"].map(num)
m["qty"] = m["qty"].map(num)
m["order_date"] = m["order_date"].map(pdate)
# 规则1:完全重复行
dup = m.duplicated(keep="first")
clean = m[~dup].copy()
# 规则2:金额勾稽 qty*price == amount
clean["calc"] = (clean["qty"]*clean["price"]).round(2)
bad_amt = clean[(clean["amount"]-clean["calc"]).abs() > 0.01]
# 规则3:必填缺失
miss = clean[["order_id","sku","qty","price","amount"]].isna().any(axis=1)
# 规则4:非法值(负数量 / 坏日期)
inv = (clean["qty"] <= 0) | clean["order_date"].isna()
# 规则5:跨表冲突(同单号多来源且金额不一致)
g = clean.groupby("order_id").agg(n_src=("__src","nunique"), n_amt=("amount","nunique"))
conf = g[(g["n_src"]>1) & (g["n_amt"]>1)]
clean[~(miss | inv | clean["order_id"].isin(conf.index))].to_excel("merged_clean.xlsx")全角/半角是大坑:
SKU、¥、千分位逗号都得先过unicodedata.normalize("NFKC")再清洗,否则金额列一转float就整列变NaN。 日期别只认一种格式:18 张表里实测出 4 种写法,多格式循环解析 + 解析失败打标记,比强制to_datetime稳。 去重要分两步:完全重复行(整行一样)先去掉,同单号多行但内容不同的留给跨表冲突规则去判断,直接drop_duplicates(subset="订单号")会把真冲突一起吞掉。 金额勾稽不要用==:浮点误差会让大量正常行误报,阈值放到0.01即可。 坏行不要删,要隔离:命中规则的行单独写进 quarantine 表,下游有人质疑时每一条都能拿出来对质,这比"干净"更重要。 顺手加一列__src记录来源文件,排查跨表冲突时能直接定位到"哪两个门店报的不一致"。
我有 18 个 xlsx 分表(路径 ./raw),表头和格式不一致。请写一条 Python 流水线:① 建列名映射字典归一表头;② 金额/数量清洗(去 ¥、千分位);③ 日期多格式解析,失败打标;④ 依次校验:完全重复行、金额勾稽(数量×单价 与金额差 >0.01)、必填缺失、非法值(负数量/坏日期)、跨表冲突(同单号多来源且金额不一致);⑤ 命中行写 merged_quarantine.xlsx,干净行写 merged_clean.xlsx;⑥ 最后输出一张命中统计表和干净/隔离行数,每个数字都要真实跑出来,不许估算。
把这段话丢给 WorkBuddy,它会自己生成数据集、写脚本、跑批、核对输出——本次实测从读入到出报告 3.14 秒,发现 1,277 处问题。合并表格这件事,"合上"只是第一步,"每一条都有据可查"才算完。
作者所处行业与岗位:信息技术服务业 · 数据与效率工具工程师(日常工作涉及多版本数据集的比对、校验与增量备份,以及批处理流程的搭建与排障)
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。