@qiao39gs
2025-07-31T10:11:40.000000Z
字数 7449
阅读 2
用python pandas对excel sheet1指定列进行去重,需保留原格式:
有四个字段(保单号、备注、GRPCONTNO、RISKCODE、BATCHNO),
按照以下规则去重:
第一步:同一GRPCONTNO有多个RISKCODE时,仅取第一次出现的riskcode,其它行的保单号修改为“/”;
第二步:根据GRPCONTNO和BATCHNO进行特殊去重:同一GRPCONTNO有多个BATCHNO时,仅取第一个batchno所在行,并将该行batchno标记,后续匹配不能再使用,该GRPCONTNO其余行的保单号修改为“/”;
这时可能会出现一种情况:所有的GRPCONTNO都已用完,还有剩余的保单号和BATCHNO(1对1),且未匹配到GRPCONTNO的保单号已被覆盖为“/”,因此要提前考虑这种情况,在筛选时若剩下则置回原第一次batchno对应的行,并将改行备注改为“缺失GRPCONTNO,该行GRPCONTNO不匹配”
请先整理下需求,有不明确点提问
我先把目前理解的需求梳理为可执行的规则,并指出需要澄清的地方。请确认或补充:
已理解的字段
- 保单号
- 备注
- GRPCONTNO
- RISKCODE
- BATCHNO
处理对象:Excel 的 sheet1
目标与规则(按步骤)
1) RISKCODE 去重(按 GRPCONTNO 维度)
- 对同一个 GRPCONTNO,如果存在多个 RISKCODE,仅保留该 GRPCONTNO 第一次出现的 RISKCODE 那一行的“保单号”,其余相同 GRPCONTNO 且不同 RISKCODE 的行,保单号改为 “/”。
- “第一次出现”基于当前行顺序(即 Excel 原序)。
2) BATCHNO 去重(按 GRPCONTNO 维度,带“已使用”的消耗逻辑)
- 对同一个 GRPCONTNO,如果存在多个 BATCHNO,仅保留该 GRPCONTNO 第一次出现的 BATCHNO 所在行,其余该 GRPCONTNO 的行,保单号改为 “/”。
- 同时将被保留的这个 BATCHNO 标记为“已使用”(后续任何 GRPCONTNO 的匹配都不能再使用这个 BATCHNO)。
- 注意:这一步是在第1步之后执行,即在 RISKCODE 已处理的基础上继续处理。
3) 特殊兜底场景:GRPCONTNO 均已“用完”,但还有剩余的保单号与 BATCHNO(一对一)无法匹配到 GRPCONTNO
- 由于第1步和第2步的处理,有可能所有可用 GRPCONTNO 都已被消耗完(或无匹配),导致剩余的保单号与 BATCHNO(1对1)“未匹配到 GRPCONTNO”,并且此类行的保单号已被覆盖为 “/”。
- 需要提前考虑并在筛选时保留一个“兜底”:若出现这种剩余,则将其中一个剩余的“第一次出现的 BATCHNO 对应的行”置回(恢复该行原始的保单号,不再是“/”),并将该行备注改为“缺失GRPCONTNO,该行GRPCONTNO不匹配”。
不明确点请确认
1) “第一次出现”的基准
- 是否以 Excel 原始行序为准?如果有表头行外,第一数据行即为第1个。
- 数据是否需按某字段排序后再做“第一次出现”的判定?目前理解为不排序,保持原序。
2) 步骤1与步骤2的先后关系和冲突处理
- 两步都可能将同一行保单号改为 “/”。是否以先执行的规则优先?目前我理解为:先做 RISKCODE 去重,再做 BATCHNO 去重;最终结果以两步叠加后的状态为准。
3) BATCHNO “已使用”的作用域
- “已使用”是否全局唯一(即不同 GRPCONTNO 也不能再使用该 BATCHNO)?还是仅对同一 GRPCONTNO 不能重复使用?文意倾向于全局唯一,请确认。
4) 步骤3的触发条件与选择哪一行“置回”
- “所有的GRPCONTNO都已用完”具体判定条件是什么?是指没有任何行还能通过 GRPCONTNO 进行有效匹配吗?还是指每个 GRPCONTNO 的“第一次 BATCHNO”都已被消耗完?
- “还有剩余的保单号和BATCHNO(1对1)”:这里的“剩余”是指哪些行?是指已被改为 “/” 的行里,仍有唯一的 BATCHNO 且与保单号一一对应?
- “置回原第一次batchno对应的行”:这里“第一次”是全表第一个剩余 BATCHNO 对应的行?还是每组挑一个?目前理解为全表仅置回一行,即剩余集合里首次出现的 BATCHNO 所在行。
- 若剩余有多行,是否只置回一行还是尽可能多地置回?当前描述像是只置回一行。请确认。
5) 备注字段
- 步骤3置回时,备注改为“缺失GRPCONTNO,该行GRPCONTNO不匹配”。其他步骤中是否需要对被改为 “/” 的行填写备注?还是保持原备注不变?
- 若原备注非空,置回时是覆盖还是在后面追加?当前理解为覆盖。
6) 保持原格式
- 最终需要保持 Excel 原格式:是否仅指数据类型与列顺序不变?是否需要保留单元格样式、宽度、颜色等?Pandas 写回一般无法完全还原样式,若要求保留样式,需使用 openpyxl 并小心处理。请明确“保留原格式”的范围。
- 输出是覆盖原 sheet1 还是写入新 sheet(比如 sheet2 或加后缀)?
7) 空值与异常
- GRPCONTNO、RISKCODE、BATCHNO 若出现空值或全空格,如何处理?空是否参与“第一次”选择?是否将空视作同一取值参与去重?
- 同一行若保单号本身就是 “/”,是否视为已被改写过,还是按正常数据处理?
- 字段大小写或前后空格是否需要统一清洗?
8) 数据示例和预期
- 能否提供一小段样例数据和预期结果,尤其覆盖以下情况:
a) 同一 GRPCONTNO 多 RISKCODE
b) 同一 GRPCONTNO 多 BATCHNO
c) BATCHNO 在不同 GRPCONTNO 之间重复,验证“已使用”的全局限制
d) 触发步骤3的“置回”场景
确认后,我可以给出稳定的 pandas 实现代码,并根据“保留原格式”的范围选用写回策略。
好的,已明确需求。关键点汇总如下,并给出可直接运行的实现方案(openpyxl 读写,按原序处理,覆盖原 sheet):
规则确认
1) 顺序基准:严格按 Excel sheet1 的原始行序处理(不排序)。
2) 步骤顺序:先按 GRPCONTNO 的 RISKCODE 去重,再按 GRPCONTNO 的 BATCHNO 去重(带全局“已使用”BATCHNO 消耗逻辑)。
3) BATCHNO “已使用”是全局唯一,任意行一旦占用该 BATCHNO,其它行都不能再用。
4) 兜底置回:
- 剩余定义:经过上两步后,保单号被改为 “/” 的行里,仍存在未使用的 BATCHNO;并且这些行在修改前的保单号是与该 BATCHNO 一一对应的(按原行即一行一对)。
- 处理:按原表顺序,尽可能多地对这些剩余行进行“置回”(恢复其原保单号),并把备注改为“缺失GRPCONTNO,该行GRPCONTNO不匹配”。
5) 备注处理:
- 被改为 “/” 的行保持原备注不变。
- 兜底置回时覆盖备注为指定文案。
6) 写回要求:使用 openpyxl,覆盖原 sheet1,保留原单元格样式、列宽等尽量不动(不新增 sheet,不调整样式)。
7) 数据完整性:字段都有值,无需清洗、大小写/空格处理。
8) 字段顺序与命名:列为“保单号、备注、GRPCONTNO、RISKCODE、BATCHNO”,按原列顺序处理与写回。
实现思路
- 读取 sheet1,逐行缓存原值(用于兜底时恢复保单号)。
- 第一步:RISKCODE 去重
对每个 GRPCONTNO,保留该 GRPCONTNO 第一次出现的 RISKCODE 那一行的保单号,其余相同 GRPCONTNO 且 RISKCODE 不同的行,保单号改为 “/”。
- 第二步:BATCHNO 去重(全局消耗)
对每个 GRPCONTNO,保留该 GRPCONTNO 第一次出现且其 BATCHNO 未被占用的那一行;将其 BATCHNO 标记为已使用。对该 GRPCONTNO 其余行保单号改为 “/”。
- 第三步:兜底置回
从头到尾扫描被改为 “/” 的行,若其 BATCHNO 仍未被使用,则对该行:
- 恢复为原保单号
- 备注改为“缺失GRPCONTNO,该行GRPCONTNO不匹配”
- 将该 BATCHNO 标记为已使用
按原序尽可能多置回,直到没有未使用的 BATCHNO。
注意:样例中显示在某些 GRPCONTNO 下出现了“第二次出现保单号未被改为 /”,这是因为在第二步按 GRPCONTNO+BATCHNO 去重时,第一个可用 BATCHNO 可能出现在该组的第二行(第一行 BATCHNO 已被全局占用),因此保留发生在该行。实现将完全按规则与原序产生此行为。
示例代码(openpyxl,仅需改文件路径和 sheet 名称即可运行)
请确保安装 openpyxl: pip install openpyxl
from openpyxl import load_workbook
def process_excel(path, sheet_name="sheet1"):
wb = load_workbook(path)
ws = wb[sheet_name]
# 假设第一行为表头,找到列索引
header = [cell.value for cell in ws[1]]
col_idx = {name: header.index(name) + 1 for name in ["保单号", "备注", "GRPCONTNO", "RISKCODE", "BATCHNO"]}
# 读取数据行(从第2行起),缓存原始保单号与行对象
rows = list(ws.iter_rows(min_row=2, values_only=False))
original_policy = {} # row_idx -> 原保单号
for r, row in enumerate(rows, start=2):
original_policy[r] = ws.cell(row=r, column=col_idx["保单号"]).value
# 第一步:RISKCODE 去重(按 GRPCONTNO,保留该GRPCONTNO首次出现的RISKCODE所在行的保单号)
first_riskcode_seen = {} # GRPCONTNO -> first RISKCODE value
for r, row in enumerate(rows, start=2):
grp = ws.cell(row=r, column=col_idx["GRPCONTNO"]).value
risk = ws.cell(row=r, column=col_idx["RISKCODE"]).value
if grp not in first_riskcode_seen:
first_riskcode_seen[grp] = risk
# 保留该行保单号
else:
if risk != first_riskcode_seen[grp]:
# 改为 "/"
ws.cell(row=r, column=col_idx["保单号"]).value = "/"
# 第二步:BATCHNO 去重(全局“已使用”)
used_batch = set()
first_kept_for_grp = set() # 记录已在该GRP保留过一行
for r, row in enumerate(rows, start=2):
grp = ws.cell(row=r, column=col_idx["GRPCONTNO"]).value
batch = ws.cell(row=r, column=col_idx["BATCHNO"]).value
pol_cell = ws.cell(row=r, column=col_idx["保单号"])
pol_val = pol_cell.value
if grp not in first_kept_for_grp:
if batch not in used_batch:
# 保留本行(不论当前是否为 "/",若为 "/" 也要“保留”即不改写,但本步主要是占用batch)
used_batch.add(batch)
first_kept_for_grp.add(grp)
# 不改备注
else:
# batch已被其它行占用,则本组还没保留成功,不能使用本行
# 将本行保单号改为 "/"
pol_cell.value = "/"
else:
# 已经在该GRP保留过一行,其余行改为 "/"
pol_cell.value = "/"
# 第三步:兜底置回(尽可能多地置回未使用的BATCHNO对应行)
for r, row in enumerate(rows, start=2):
pol_cell = ws.cell(row=r, column=col_idx["保单号"])
if pol_cell.value == "/":
batch = ws.cell(row=r, column=col_idx["BATCHNO"]).value
if batch not in used_batch:
# 置回
pol_cell.value = original_policy[r]
# 备注覆盖
ws.cell(row=r, column=col_idx["备注"]).value = "缺失GRPCONTNO,该行GRPCONTNO不匹配"
used_batch.add(batch)
wb.save(path)
说明
- 代码按原行序遍历,不排序。
- 保留原样式:openpyxl 修改单元格值,默认不会改变已有样式、列宽、颜色等。
- 覆盖原 sheet:直接写回同一工作表并保存。
- 兜底是“尽可能多地置回”:按原序遇到未被使用的 BATCHNO 就恢复该行保单号并覆盖备注。
如需对日志/检测进行增强(例如打印每步保留/改写的行号),可加简单的 print 或记录列表。若你的真实数据列名与顺序不同,请告知,我会调整列名定位方式。若要在保存前先输出一个预览(不落盘),也可加导出为新文件的选项。
能否猜测下原本意图?还是不好描述去重的目的和规则
可以做一个合理的“业务意图”解读,帮助用更易理解的目标来描述规则。结合你的样例和约束,我猜测原本意图大致是:
总体目标
在同一批数据中,尽量为每个团单(GRPCONTNO)选出一条“代表性”记录,占用一个全局唯一且未被使用过的批次号(BATCHNO),并优先采用该团单首次出现的产品/险种(RISKCODE)对应的记录;若因为全局 BATCHNO 冲突导致很多记录被压制,则把仍未被占用的 BATCHNO 尽可能分配给剩余的记录,即使它们的 GRPCONTNO 已经不再有效匹配,也要把这些记录“救回来”,并在备注里说明“GRPCONTNO 不匹配”。
换句话说:
1) 先选代表:每个 GRPCONTNO 只留一条,且这条记录应使用该 GRPCONTNO 首次出现的 RISKCODE(体现“原始顺序优先”和“稳定代表”的想法)。
2) 控制资源:每个 BATCHNO 在全局只能用一次,避免重复占用(像是“批次资源”的唯一分配)。
3) 最大化利用:如果按上面两条后仍有未被使用的 BATCHNO(资源剩余),就把这些资源尽可能分配出去给被压制的记录,让它们“活”回来,并明确标注因为 GRPCONTNO 不匹配而“特殊放行”。
为什么有这套规则的可能原因
- RISKCODE 维度的首次优先:避免同一团单下多险种重复计入,取最早出现的险种作为该团单的代表。
- BATCHNO 全局唯一:批次号可能对应后续的对账、上传或下游系统的唯一键,必须防止重复。
- 兜底置回:业务上不希望丢失可用的批次资源;当“团单-批次”的理想匹配做不满时,宁愿以“备注说明”的方式把数据放出来,满足尽可能多的“保单-批次”绑定,保证覆盖率或产出规模。
用一句话概括
在原始顺序下,为每个团单选择一个首险种的代表记录,占用唯一批次;若还有未被使用的批次资源,则尽量回填到被压制的记录上,并标注其团单不匹配的原因,从而在保证唯一性的同时最大化数据利用率。
如果你要对外简洁表达规则
- 每个 GRPCONTNO 只保留与其首次出现的 RISKCODE 对应的一条记录;
- BATCHNO 全局唯一,一旦被占用其他行不可再用;
- 若仍有未被使用的 BATCHNO,则按原序尽量回填给已被覆盖的行,并在备注中标明“缺失GRPCONTNO,该行GRPCONTNO不匹配”。