@qiao39gs
2025-08-07T08:07:46.000000Z
字数 19345
阅读 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 或记录列表。若你的真实数据列名与顺序不同,请告知,我会调整列名定位方式。若要在保存前先输出一个预览(不落盘),也可加导出为新文件的选项。
现修改需求,excel原为两csv结合产生,请从csv开始进行处理,不再是在excel上进行修改:
csv文件:a.csv
grpcontno riskcode transdate managecom agentcom agentcode finmoney
csv文件:b.csv
保单号 batchno syndate valdate riskcode managecom agentcom agentcode chnltype salechnl subchnl polmoney
使用python pandas进行数据处理:
两csv关联条件:
a.riskcode=b.riskcode
a.finmoney=b.polmoney
a.managecom=b.managecom
a.agentcom=b.agentcom
a.agentcode=b.agentcode
a.transdate=b.syndate
[处理过程]
最终输出:
匹配:保单号、grpcontno、batchno及其它字段
无可用grpcontno:保单号、batchno及其它字段
好的,需求已变更。我们将从两个 CSV 文件开始,进行匹配和去重处理,最终生成一个包含所有结果的输出文件。
输入:
a.csv: 包含 grpcontno (团体合同号) 和关联键。b.csv: 包含 保单号、batchno 和关联键。核心任务:数据关联
a.csv 中的 grpcontno 匹配到 b.csv 的每一行数据上。a.riskcode = b.riskcodea.finmoney = b.polmoneya.managecom = b.managecoma.agentcom = b.agentcoma.agentcode = b.agentcodea.transdate = b.syndate核心任务:去重与分配 (Allocation)
b.csv 中的某一行(代表一个唯一的 保单号-batchno 组合)可能会匹配到 a.csv 中的多行(即多个 grpcontno)。保单号-batchno 分配一个唯一的 grpcontno。grpcontno 只能被分配一次。一个 batchno 也只能被分配一次。b.csv 的原始行顺序,进行逐行尝试分配。对于 b.csv 的某一行,如果它匹配到了多个可用的 grpcontno,则选择按 a.csv 原始顺序排在最前面的那个 grpcontno。b.csv 的第一行开始。grpcontno。grpcontno,选择第一个尚未被分配的 grpcontno 进行配对。grpcontno 和该行对应的 batchno 就被视为“已使用”,不能再用于任何其他分配。b.csv 的下一行。输出结果:
grpcontno 的行。包含 保单号, batchno, grpcontno 及其他所有字段。grpcontno 的行 (可能是因为没有匹配项,或匹配到的 grpcontno 已被占用)。这些行应包含原始的 保单号, batchno 及其他 b.csv 中的字段,同时 grpcontno 字段为空,并有一个“备注”字段说明情况。b.csv 所有字段,加上 a.csv 的 grpcontno 和一个我们自己创建的 备注 字段。我们将采用以下步骤来实现这个逻辑:
a.csv 和 b.csv。b.csv 数据添加一个临时索引列,以确保在合并后可以恢复其原始顺序。pd.merge 并指定 how='left',将 b.csv (left) 与 a.csv (right) 根据所有关联条件进行合并。这会保留 b.csv 的所有行,并为能匹配上的行附加上 grpcontno。b.csv 的原始顺序对合并后的数据进行排序。grpcontno 和 batchno,筛选出“黄金匹配”的行。b.csv 中存在,但其 batchno 未被成功分配的行。
import pandas as pdimport numpy as np# --- 1. 配置与加载数据 ---# 定义文件路径file_a_path = 'a.csv'file_b_path = 'b.csv'output_file_path = 'result.csv'# 定义关联键# 注意:左右文件的列名不同,需要分别指定left_keys = ['riskcode', 'polmoney', 'managecom', 'agentcom', 'agentcode', 'syndate']right_keys = ['riskcode', 'finmoney', 'managecom', 'agentcom', 'agentcode', 'transdate']# 使用 try-except 块来处理文件不存在的错误try:df_a = pd.read_csv(file_a_path)df_b = pd.read_csv(file_b_path)except FileNotFoundError as e:print(f"错误:找不到文件 {e.filename}。请确保 a.csv 和 b.csv 文件在当前目录下。")# 创建示例文件以便脚本能继续运行print("正在创建示例 a.csv 和 b.csv 文件...")# 示例数据 a.csvdata_a = {'grpcontno': ['SL001', 'SL001', 'SL002', 'SL003'],'riskcode': [619, 806, 619, 619],'transdate': ['2023-01-01', '2023-01-01', '2023-01-01', '2023-01-01'],'managecom': ['COM1', 'COM1', 'COM1', 'COM1'],'agentcom': ['AGENT_A', 'AGENT_A', 'AGENT_A', 'AGENT_A'],'agentcode': ['A001', 'A001', 'A001', 'A001'],'finmoney': [1000.0, 1000.0, 2000.0, 3000.0]}df_a = pd.DataFrame(data_a)df_a.to_csv(file_a_path, index=False)# 示例数据 b.csvdata_b = {'保单号': [100, 101, 102, 103],'batchno': ['B001', 'B002', 'B003', 'B004'],'syndate': ['2023-01-01', '2023-01-01', '2023-01-01', '2023-01-01'],'valdate': ['2023-01-10', '2023-01-11', '2023-01-12', '2023-01-13'],'riskcode': [619, 619, 806, 999], # B003 匹配 riskcode 806, B004 无匹配'managecom': ['COM1', 'COM1', 'COM1', 'COM1'],'agentcom': ['AGENT_A', 'AGENT_A', 'AGENT_A', 'AGENT_A'],'agentcode': ['A001', 'A001', 'A001', 'A001'],'chnltype': ['C1', 'C1', 'C1', 'C1'],'subchnl': ['S1', 'S1', 'S1', 'S1'],'polmoney': [1000.0, 2000.0, 1000.0, 5000.0] # B002 匹配 SL002, B001/B003 匹配 SL001}df_b = pd.DataFrame(data_b)df_b.to_csv(file_b_path, index=False)print("示例文件创建成功,请重新运行脚本。")exit()# --- 2. 保留原始顺序并合并 ---# 为 df_b 添加原始索引df_b['original_index'] = df_b.index# 左连接,保留所有 b.csv 的记录merged_df = pd.merge(df_b,df_a,how='left',left_on=left_keys,right_on=right_keys)# 恢复 b.csv 的原始顺序。如果一行 b 匹配多行 a, 它们的相对顺序由 a 的原始顺序决定merged_df = merged_df.sort_values(by='original_index').reset_index(drop=True)# --- 3. 执行分配逻辑 ---used_grpcontno = set()used_batchno = set()matched_rows = []# itertuples() 是比 iterrows() 更高效的遍历方式for row in merged_df.itertuples(index=False):# 检查是否是潜在的有效匹配# pd.notna() 用于处理 grpcontno 可能为 NaN 的情况if pd.notna(row.grpcontno) and row.grpcontno not in used_grpcontno and row.batchno not in used_batchno:# 这是一个成功的分配used_grpcontno.add(row.grpcontno)used_batchno.add(row.batchno)# 将行数据(เป็น namedtuple)转换为字典并添加到结果列表matched_rows.append(row._asdict())# 创建成功匹配的 DataFramedf_matched = pd.DataFrame(matched_rows)# --- 4. 识别并准备未分配的行 ---# 从原始 df_b 中,筛选出那些 batchno 未被使用的行unmatched_mask = ~df_b['batchno'].isin(used_batchno)df_unmatched = df_b[unmatched_mask].copy()# 为未匹配的行添加备注if not df_unmatched.empty:df_unmatched['备注'] = '无可用grpcontno'# --- 5. 整合与格式化 ---# 准备拼接。df_matched 可能包含 df_a 的列,而 df_unmatched 不包含# 我们需要确保两者的列一致final_columns = list(df_b.columns) + ['grpcontno', '备注'] # 定义期望的最终列# 为 df_matched 添加空的“备注”列(如果需要)if '备注' not in df_matched.columns:df_matched['备注'] = ''# 组合最终结果# concat 会自动处理列的对齐,缺失值填充为 NaNfinal_df = pd.concat([df_matched, df_unmatched], ignore_index=True)# 清理和整理列# 移除合并过程中产生的重复列(例如 riskcode_y),只保留原始的cols_to_drop = ['original_index'] + [col for col in final_df.columns if col.endswith('_y')]final_df = final_df.drop(columns=cols_to_drop, errors='ignore')# 重命名 x 列以去除后缀final_df = final_df.rename(columns={col: col.replace('_x', '') for col in final_df.columns if col.endswith('_x')})# 获取 df_b 和 df_a 中不重复的列名,以构建最终的列顺序b_cols = [col for col in df_b.columns if col != 'original_index']a_cols_unique = [col for col in df_a.columns if col not in df_b.columns]# 定义最终的列顺序,将重要字段放在前面ordered_cols = ['保单号', 'batchno', 'grpcontno', '备注']other_cols = [col for col in b_cols + a_cols_unique if col not in ordered_cols]final_ordered_cols = ordered_cols + other_cols# 确保所有期望的列都存在,不存在的则用 NaN 填充for col in final_ordered_cols:if col not in final_df.columns:final_df[col] = np.nanfinal_df = final_df[final_ordered_cols]# --- 6. 输出到 CSV 文件 ---final_df.to_csv(output_file_path, index=False, encoding='utf-8-sig')print(f"处理完成!结果已保存到:{output_file_path}")print("\n--- 最终结果预览 ---")print(final_df.head(10))
a.csv 和 b.csv 文件放在与此 Python 脚本相同的目录下。pip install pandas。result.csv 的文件,这就是最终的处理结果。该文件可以用 Excel 直接打开,不会有乱码问题。try-except 块。如果 a.csv或b.csv 不存在,它会提示错误并自动创建两个符合格式的示例文件,方便你理解数据结构和测试脚本。df_b['original_index'] = df_b.index 是关键一步,它确保了无论合并过程如何打乱数据,我们都能恢复 b.csv 的原始处理顺序。itertuples(): 这是一个高效的行遍历方法,比 iterrows() 快得多,特别适合处理大型数据集。used_grpcontno 和 used_batchno 使用了 Python 的 set 数据结构,它的查找操作(如 in)平均时间复杂度为 O(1),非常高效。pd.concat: 这是 Pandas 中用于沿某个轴将多个对象(如 DataFrame)堆叠在一起的标准方法。它能智能地对齐列,是合并不同结构数据的首选。_x 和 _y 后缀。代码的最后部分负责清理这些多余的列和后缀,并按逻辑顺序重新排列,使输出文件更清晰。改为与excel相同的字段顺序:
保单号 备注 grpcontno riskcode transdate managecom agentcom agentcode finmoney batchno syndate valdate riskcode managecom agentcom agentcode chnltype salechnl subchnl polmoney
好的,没有问题。我们将在上一个脚本的基础上,修改最后一步的列排序逻辑,以完全匹配您提供的字段顺序。
核心的改动在于:我们将直接使用您提供的列名列表来定义最终输出文件的列顺序,而不是动态生成。
您提供的顺序是:
保单号 备注 grpcontno riskcode transdate managecom agentcom agentcode finmoney batchno syndate valdate riskcode managecom agentcom agentcode chnltype salechnl subchnl polmoney
这里面有一些重复的列名(如 riskcode, managecom 等),这些是关联键。在最终的 DataFrame 中,每个列名只能出现一次。我们将保留这个列表中的唯一列名,并保持它们的首次出现顺序。
例如,riskcode 出现了两次,但我们最终的 DataFrame 只会有一列叫 riskcode。dict.fromkeys() 是一个很方便的方法,可以从列表中创建唯一项,同时保留顺序(在 Python 3.7+ 中)。
这是完整的、可以直接运行的脚本。您只需将 a.csv 和 b.csv 放在同一目录下即可。
import pandas as pdimport numpy as np# --- 1. 配置与加载数据 ---# 定义文件路径file_a_path = 'a.csv'file_b_path = 'b.csv'output_file_path = 'result_ordered.csv'# 定义关联键left_keys = ['riskcode', 'polmoney', 'managecom', 'agentcom', 'agentcode', 'syndate']right_keys = ['riskcode', 'finmoney', 'managecom', 'agentcom', 'agentcode', 'transdate']# 定义用户指定的最终列顺序user_specified_order_str = ("保单号 备注 grpcontno riskcode transdate managecom agentcom ""agentcode finmoney batchno syndate valdate chnltype salechnl ""subchnl polmoney")# 修正并去重用户列表(注意:我已根据逻辑移除了重复的关联键)final_ordered_cols = list(dict.fromkeys(user_specified_order_str.split()))# 使用 try-except 块来处理文件不存在的错误try:df_a = pd.read_csv(file_a_path)df_b = pd.read_csv(file_b_path)except FileNotFoundError as e:print(f"错误:找不到文件 {e.filename}。请确保 a.csv 和 b.csv 文件在当前目录下。")# 创建示例文件以便脚本能继续运行print("正在创建示例 a.csv 和 b.csv 文件...")data_a = {'grpcontno': ['SL001', 'SL001', 'SL002', 'SL003'],'riskcode': [619, 806, 619, 619],'transdate': ['2023-01-01', '2023-01-01', '2023-01-01', '2023-01-01'],'managecom': ['COM1', 'COM1', 'COM1', 'COM1'],'agentcom': ['AGENT_A', 'AGENT_A', 'AGENT_A', 'AGENT_A'],'agentcode': ['A001', 'A001', 'A001', 'A001'],'finmoney': [1000.0, 1000.0, 2000.0, 3000.0]}df_a = pd.DataFrame(data_a)df_a.to_csv(file_a_path, index=False)data_b = {'保单号': [100, 101, 102, 103],'batchno': ['B001', 'B002', 'B003', 'B004'],'syndate': ['2023-01-01', '2023-01-01', '2023-01-01', '2023-01-01'],'valdate': ['2023-01-10', '2023-01-11', '2023-01-12', '2023-01-13'],'riskcode': [619, 619, 806, 999],'managecom': ['COM1', 'COM1', 'COM1', 'COM1'],'agentcom': ['AGENT_A', 'AGENT_A', 'AGENT_A', 'AGENT_A'],'agentcode': ['A001', 'A001', 'A001', 'A001'],'chnltype': ['C1', 'C1', 'C1', 'C1'],'salechnl': ['CH1', 'CH1', 'CH1', 'CH1'], # 添加了 salechnl 以匹配列顺序'subchnl': ['S1', 'S1', 'S1', 'S1'],'polmoney': [1000.0, 2000.0, 1000.0, 5000.0]}df_b = pd.DataFrame(data_b)df_b.to_csv(file_b_path, index=False)print("示例文件创建成功,请重新运行脚本。")exit()# --- 2. 保留原始顺序并合并 ---df_b['original_index'] = df_b.indexmerged_df = pd.merge(df_b,df_a,how='left',left_on=left_keys,right_on=right_keys,suffixes=('', '_a') # 为 a 表的重名列添加后缀 _a)merged_df = merged_df.sort_values(by='original_index').reset_index(drop=True)# --- 3. 执行分配逻辑 ---used_grpcontno = set()used_batchno = set()matched_rows = []for row in merged_df.itertuples(index=False):if pd.notna(row.grpcontno) and row.grpcontno not in used_grpcontno and row.batchno not in used_batchno:used_grpcontno.add(row.grpcontno)used_batchno.add(row.batchno)matched_rows.append(row._asdict())df_matched = pd.DataFrame(matched_rows)# --- 4. 识别并准备未分配的行 ---unmatched_mask = ~df_b['batchno'].isin(used_batchno)df_unmatched = df_b[unmatched_mask].copy()if not df_unmatched.empty:df_unmatched['备注'] = '无可用grpcontno'# --- 5. 整合与格式化 ---if '备注' not in df_matched.columns:df_matched['备注'] = ''final_df = pd.concat([df_matched, df_unmatched], ignore_index=True)# 清理合并时产生的辅助列和重复列cols_to_drop = ['original_index'] + [col for col in final_df.columns if col.endswith('_a')]final_df = final_df.drop(columns=cols_to_drop, errors='ignore')# 确保所有期望的列都存在于DataFrame中,不存在则用空值填充for col in final_ordered_cols:if col not in final_df.columns:final_df[col] = np.nan# 最终,使用用户指定的顺序来选择和排序最终的列final_df = final_df[final_ordered_cols]# --- 6. 输出到 CSV 文件 ---final_df.to_csv(output_file_path, index=False, encoding='utf-8-sig')print(f"处理完成!结果已保存到:{output_file_path}")print("\n--- 最终结果预览 (按指定顺序) ---")print(final_df.head(10))
定义最终列顺序:
user_specified_order_str = ("保单号 备注 grpcontno riskcode transdate managecom agentcom ""agentcode finmoney batchno syndate valdate chnltype salechnl ""subchnl polmoney")# 修正并去重用户列表final_ordered_cols = list(dict.fromkeys(user_specified_order_str.split()))
list(dict.fromkeys(...)) 是一个高效的技巧,用于去除列表中的重复项,同时保持元素的原始顺序。这确保了像 riskcode 这样的列只在最终列表中出现一次。合并策略调整:
merged_df = pd.merge(...suffixes=('', '_a') # 为 a 表的重名列添加后缀 _a)
pd.merge 中增加了 suffixes 参数。suffixes=('', '_a') 意味着,如果 df_b 和 df_a 有重名列(如 riskcode),df_b 中的列名保持不变,而 df_a 中的列名会被加上 _a 的后缀(变成 riskcode_a)。这使得后续的列清理更加清晰。最终列选择和排序:
# 确保所有期望的列都存在于DataFrame中...for col in final_ordered_cols:if col not in final_df.columns:final_df[col] = np.nan# 最终,使用用户指定的顺序来选择和排序最终的列final_df = final_df[final_ordered_cols]
NaN 填充)。final_df = final_df[final_ordered_cols] 这一行代码会按照 final_ordered_cols 列表中定义的顺序,重新排列并选择 DataFrame 的列,得到完全符合您要求的最终结果。现在,运行此脚本生成的 result_ordered.csv 文件,其列顺序将严格按照您的要求排列。
完整梳理下当前需求
好的,我们来完整地梳理一下当前最终确认的需求。这可以作为一份项目备忘录或需求规格说明。
将来自两个不同数据源(a.csv 和 b.csv)的数据进行关联,核心目标是将团体合同号(grpcontno)准确地分配给符合条件的保单记录。由于资源(grpcontno 和 batchno)的唯一性限制,需要根据一套优先级规则来进行分配,并最终生成一份包含所有保单记录(无论是否成功分配)的完整报告。
整个流程从两个 CSV 文件开始:
文件一:a.csv (团体合同信息)
grpcontno 及关联匹配所需的主数据。grpcontno, riskcode, transdate, managecom, agentcom, agentcode, finmoney。grpcontno 的出现顺序具有业务意义,将作为分配时的优先依据。文件二:b.csv (保单批次信息)
grpcontno 的所有保单记录。保单号, batchno, syndate, valdate, riskcode, managecom, agentcom, agentcode, chnltype, salechnl, subchnl, polmoney。处理过程需严格按照以下步骤和规则执行:
数据关联 (Left Merge)
b.csv 的所有记录为基础(左表),与 a.csv(右表)进行左连接匹配。b.riskcode = a.riskcodeb.polmoney = a.finmoneyb.managecom = a.managecomb.agentcom = a.agentcomb.agentcode = a.agentcodeb.syndate = a.transdateb.csv 的某一行可能会因匹配到 a.csv 的多行而出现重复。分配与去重 (核心算法)
b.csv 的原始行顺序进行遍历和决策。grpcontno 在整个流程中只能被成功分配一次。batchno 在整个流程中也只能被成功分配一次。b.csv 的第一行对应的候选匹配开始。 grpcontno(这些 grpcontno 的相对顺序由它们在 a.csv 中的原始顺序决定)。 grpcontno,选择第一个尚未被使用的 grpcontno。 grpcontno,并且当前行的 batchno 也尚未被使用,则视为一次成功分配。 grpcontno 和这个 batchno 标记为“已使用”。 b.csv 的原始顺序处理下一行。 grpcontno 都已被占用,或者该行的 batchno 已被占用,则该行的所有候选匹配都作废,无法成功分配。result_ordered.csv 的单一 CSV 文件。UTF-8-SIG,以确保在 Excel 中打开时中文不会乱码。内容: 文件必须包含 b.csv 的所有原始记录,并根据分配结果呈现为两种状态:
b.csv 的所有字段。grpcontno 被填入 grpcontno 列。备注 列为空。b.csv 的所有字段。grpcontno 列为空 (NaN)。备注 列的值必须为 "无可用grpcontno"。字段顺序: 输出的 CSV 文件列顺序必须严格按照以下规定排列:
保单号, 备注, grpcontno, riskcode, transdate, managecom, agentcom, agentcode, finmoney, batchno, syndate, valdate, chnltype, salechnl, subchnl, polmoney
transdate 和 finmoney 来自 a.csv,其余字段主要来自 b.csv。如果某行未成功匹配,则 transdate 和 finmoney 列也为空。这份梳理总结了从数据源到最终输出的完整流程、所有业务规则和格式要求,可以作为后续开发和验证的依据。