2. 排查 Excel 多表头与合并单元格导致的列名异常
问题定位:多行表头与合并单元格如何破坏列名
含多行表头与合并单元格的 Excel 文件,在 pandas 默认读取时会出现三类典型异常:列名重复(合并单元格展开后多列同名)、NaN 列名(空白单元格被读成未命名列)、数据偏移(真正的表头行被当成数据行,或数据行被当成表头)。核心原因是 read_excel 默认 header=0,只取第一行作列名,而合并单元格在 Excel 中只有左上角单元格有值,其余为空白。
复现步骤与关键代码
下面用一份构造的示例文件 sales_multiheader.xlsx 演示完整修复流程。示例数据为教学构造,非真实业务数据。
import pandas as pd
import numpy as np
# 1) 先"裸读"看结构:不设 header,观察前几行
raw = pd.read_excel("sales_multiheader.xlsx", header=None, nrows=5)
print(raw)
# 预期:第0行大标题、第1行分组、第2行真实字段、第3行起为数据
# 2) 定位真实字段行:找到第一行"非空单元格最多"的行
non_null = raw.notna().sum(axis=1)
header_row = int(non_null.idxmax())
print("真实字段行索引:", header_row) # 示例中应为 2
# 3) 用 skiprows + header 正确读入
df = pd.read_excel("sales_multiheader.xlsx", skiprows=header_row, header=0)
print(df.columns.tolist())
# 预期:['销售额','订单数','销售额','订单数','状态','负责人'] —— 仍有重复
# 4) 列名清洗:去空白、去 Unnamed、重复加后缀
def clean_columns(cols):
out, seen = [], {}
for c in cols:
name = str(c).strip()
if name == "" or name.lower().startswith("unnamed"):
name = "col"
if name in seen:
seen[name] += 1
name = f"{name}_{seen[name]}"
else:
seen[name] = 0
out.append(name)
return out
df.columns = clean_columns(df.columns)
print(df.columns.tolist())
# 预期:['销售额','订单数','销售额_1','订单数_1','状态','负责人']
# 5) 校验
assert df.columns.is_unique, "列名仍有重复"
assert not df.columns.isna().any(), "存在 NaN 列名"
assert df.shape[0] == len(pd.read_excel("sales_multiheader.xlsx", header=None)) - header_row - 1
print("校验通过,行数:", df.shape[0])
若分组表头(华东/华南)需要保留语义,可改用
header=[0,1] 读成 MultiIndex,再用 df.columns = ['_'.join(map(str, c)).strip() for c in df.columns] 扁平化,得到 华东_销售额 这类可读列名。常见坑:
skiprows 与 header 同时使用时,header 是相对于跳过后的行计数。若 skiprows=2, header=0,则原第3行(索引2)成为列名,原第4行起为数据——务必用 nrows 先验证,避免数据行被误当表头。结果判读与验证要点
| 检查项 | 通过标准 | 失败时的典型原因 |
|---|---|---|
| 列名唯一性 | df.columns.is_unique == True | 合并单元格展开后同名,未加后缀 |
| 无 NaN 列名 | not df.columns.isna().any() | 空白单元格被读成 Unnamed/NaN |
| 行数一致 | 读入行数 = 源表总行数 − 表头行数 | header 定位错误导致数据行被吞 |
| dtype 合理 | 数值列非 object | 表头行混入数据,整列变字符串 |