[关闭]
@qiao39gs 2025-07-31T10:11:40.000000Z 字数 7449 阅读 2

整理需求去重规则澄清问题

🧑‍💻 User

用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不匹配”

请先整理下需求,有不明确点提问


🤖 Assistant

我先把目前理解的需求梳理为可执行的规则,并指出需要澄清的地方。请确认或补充:

已理解的字段
- 保单号
- 备注
- 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 实现代码,并根据“保留原格式”的范围选用写回策略。


🧑‍💻 User

  1. 以 Excel 原始行序为准,按照原序处理
  2. 理解正确
  3. 全局唯一

  4. 也可以说还有未被使用的BATCHNO
    是指已被改为 “/” 的行里,仍有唯一的 BATCHNO 且与保单号一一对应(指的是修改前的保单号)
    全表第一个剩余 BATCHNO 对应的行
    尽可能多地置回
    5.
    被改为 “/” 的行保持原备注不变
    覆盖原备注

  5. 改为使用 openpyxl 处理
    覆盖原sheet
    7.都不用考虑,都有值且无需清洗
    8.示例:
    处理前:
    保单号 备注 GRPCONTNO RISKCODE BATCHNO
    100 SL001 619 001
    101 SL001 619 002
    102 SL001 619 003
    103 SL001 619 004
    100 SL001 806 001
    101 SL001 806 002
    102 SL001 806 003
    103 SL001 806 004
    100 SL002 619 001
    101 SL002 619 002
    102 SL002 619 003
    103 SL002 619 004
    100 SL002 806 001
    101 SL002 806 002
    102 SL002 806 003
    103 SL002 806 004
    100 SL003 619 001
    101 SL003 619 002
    102 SL003 619 003
    103 SL003 619 004
    100 SL003 806 001
    101 SL003 806 002
    102 SL003 806 003
    103 SL003 806 004
    处理后:
    保单号 备注 GRPCONTNO RISKCODE BATCHNO
    100 SL001 619 001
    / SL001 619 002
    / SL001 619 003
    103 缺失GRPCONTNO,该行GRPCONTNO不匹配 SL001 619 004
    / SL001 806 001
    / SL001 806 002
    / SL001 806 003
    / SL001 806 004
    / SL002 619 001
    101 SL002 619 002
    / SL002 619 003
    / SL002 619 004
    / SL002 806 001
    / SL002 806 002
    / SL002 806 003
    / SL002 806 004
    / SL003 619 001
    / SL003 619 002
    102 SL003 619 003
    / SL003 619 004
    / SL003 806 001
    / SL003 806 002
    / SL003 806 003
    / SL003 806 004

🤖 Assistant

好的,已明确需求。关键点汇总如下,并给出可直接运行的实现方案(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)

用法

process_excel("your_file.xlsx", "sheet1")

说明
- 代码按原行序遍历,不排序。
- 保留原样式:openpyxl 修改单元格值,默认不会改变已有样式、列宽、颜色等。
- 覆盖原 sheet:直接写回同一工作表并保存。
- 兜底是“尽可能多地置回”:按原序遇到未被使用的 BATCHNO 就恢复该行保单号并覆盖备注。

如需对日志/检测进行增强(例如打印每步保留/改写的行号),可加简单的 print 或记录列表。若你的真实数据列名与顺序不同,请告知,我会调整列名定位方式。若要在保存前先输出一个预览(不落盘),也可加导出为新文件的选项。


🧑‍💻 User

能否猜测下原本意图?还是不好描述去重的目的和规则


🤖 Assistant

可以做一个合理的“业务意图”解读,帮助用更易理解的目标来描述规则。结合你的样例和约束,我猜测原本意图大致是:

总体目标
在同一批数据中,尽量为每个团单(GRPCONTNO)选出一条“代表性”记录,占用一个全局唯一且未被使用过的批次号(BATCHNO),并优先采用该团单首次出现的产品/险种(RISKCODE)对应的记录;若因为全局 BATCHNO 冲突导致很多记录被压制,则把仍未被占用的 BATCHNO 尽可能分配给剩余的记录,即使它们的 GRPCONTNO 已经不再有效匹配,也要把这些记录“救回来”,并在备注里说明“GRPCONTNO 不匹配”。

换句话说:
1) 先选代表:每个 GRPCONTNO 只留一条,且这条记录应使用该 GRPCONTNO 首次出现的 RISKCODE(体现“原始顺序优先”和“稳定代表”的想法)。
2) 控制资源:每个 BATCHNO 在全局只能用一次,避免重复占用(像是“批次资源”的唯一分配)。
3) 最大化利用:如果按上面两条后仍有未被使用的 BATCHNO(资源剩余),就把这些资源尽可能分配出去给被压制的记录,让它们“活”回来,并明确标注因为 GRPCONTNO 不匹配而“特殊放行”。

为什么有这套规则的可能原因
- RISKCODE 维度的首次优先:避免同一团单下多险种重复计入,取最早出现的险种作为该团单的代表。
- BATCHNO 全局唯一:批次号可能对应后续的对账、上传或下游系统的唯一键,必须防止重复。
- 兜底置回:业务上不希望丢失可用的批次资源;当“团单-批次”的理想匹配做不满时,宁愿以“备注说明”的方式把数据放出来,满足尽可能多的“保单-批次”绑定,保证覆盖率或产出规模。

用一句话概括
在原始顺序下,为每个团单选择一个首险种的代表记录,占用唯一批次;若还有未被使用的批次资源,则尽量回填到被压制的记录上,并标注其团单不匹配的原因,从而在保证唯一性的同时最大化数据利用率。

如果你要对外简洁表达规则
- 每个 GRPCONTNO 只保留与其首次出现的 RISKCODE 对应的一条记录;
- BATCHNO 全局唯一,一旦被占用其他行不可再用;
- 若仍有未被使用的 BATCHNO,则按原序尽量回填给已被覆盖的行,并在备注中标明“缺失GRPCONTNO,该行GRPCONTNO不匹配”。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注