
本文介绍一种基于pandas的稳健方案,将excel中任意数量的工作表提取“roll number”和“brief”列,并以每个表名为新列名、按学号对齐合并为单张宽表,彻底解决传统逐次merge导致的笛卡尔积式重复问题。
在处理教学或教务类Excel数据时,常遇到多个班级(如ClassA、ClassB)分表存储的情况:每张表结构相似但行数不一,且同一“Roll Number”可能在单表内出现多次(例如一名学生选修多门课)。目标是将所有表的“Brief”信息横向展开,以“Roll Number”为行索引、各表名为列名,形成规整的宽表——关键在于:同一学号的多条记录需严格按出现顺序一一对应,而非交叉匹配。
原方法使用循环pd.merge(..., how='outer')的问题在于:它对相同Roll Number的所有行进行全组合(Cartesian product),导致11号学生在ClassA有2条、ClassB有1条时,生成2行重复的11号记录(如Maths 11 + Art 71 和 Science 12 + Art 71),违背业务逻辑。
✅ 正确思路是:为每个Roll Number组内的记录添加序号标签(cumcount),再以(Roll Number, 序号)为联合索引进行拼接。这样能保证“第1次出现的11号→ClassA第1条,ClassB第1条”,“第2次出现的11号→ClassA第2条,ClassB第2条”,缺失则填充NaN。
以下是完整可运行代码:
import pandas as pd
# 步骤1:一次性读取所有工作表为字典(键=表名,值=DataFrame)
df_dict = pd.read_excel("input.xlsx", sheet_name=None)
# 步骤2:对每张表提取关键列,并添加组内序号作为辅助索引
dfs = []
for sheet_name, df in df_dict.items():
# 仅保留必要列,避免列名冲突
subset = df[["Roll Number", "Brief"]].copy()
# 为每个Roll Number组内行编号:0, 1, 2...
subset["seq"] = subset.groupby("Roll Number").cumcount()
# 设置复合索引:(Roll Number, seq),并重命名Brief列为表名
subset = subset.set_index(["Roll Number", "seq"]).rename(columns={"Brief": sheet_name})
dfs.append(subset)
# 步骤3:沿列方向拼接(axis=1),丢弃二级索引"seq",重置索引
result = pd.concat(dfs, axis=1).droplevel("seq").reset_index()
# 步骤4:输出结果(可选:按Roll Number排序)
result = result.sort_values("Roll Number").reset_index(drop=True)
result.to_excel("combined_output.xlsx", index=False)
print(result)输出示例:
Roll Number ClassA ClassB 0 11 Maths 11 Art 71 1 11 Science 12 NaN 2 12 History Maths 3 13 NaN Science 12
? 关键点说明:
- groupby("Roll Number").cumcount() 是核心:它为每个学号的首次出现标0、第二次标1……确保跨表对齐逻辑一致;
- set_index(["Roll Number", "seq"]) 构建唯一行标识,避免merge歧义;
- pd.concat(..., axis=1) 比循环merge更高效、更可控,天然支持多表对齐;
- .droplevel("seq") 移除辅助索引后,Roll Number成为唯一行索引,符合最终需求。
⚠️ 注意事项:
- 确保所有工作表中“Roll Number”列名完全一致(区分大小写与空格);
- 若存在空值,cumcount()会将其计入计数(即NaN也参与分组编号),建议提前清洗:df.dropna(subset=["Roll Number"]);
- 如需保留原始顺序(非按学号排序),删除最后的sort_values即可;
- 内存敏感场景下,可对大文件分批处理,但本方案对万级行数据性能依然优秀。
该方法兼顾正确性、可读性与扩展性,是处理多表“一对多”对齐合并任务的标准实践。










