Python Pandas 实战:复杂业务逻辑下的数据对账与清洗策略
在企业级数据处理中,我们经常面临“多源数据对账”的场景。比如:系统A是原始订单,系统B是调度或分发系统,我们需要核对两个系统中的订单状态、数量以及关键日期是否一致。
看似简单的 merge 操作,在实际落地时往往会遇到各种“坑”:
- 关联键格式不统一:有的带后缀,有的没带。
- 时间戳越界:遗留系统常用
9999-12-31或3000-01-01表示“永久”,导致 Pandas 报Out of bounds nanosecond timestamp。 - 复杂的行级逻辑:状态判定不仅仅看某一列,而是涉及数量、标志位、日期等多列的组合逻辑。
- 合并后的列名冲突:分不清
_x和_y,导致取错数据。
本文将结合一段 Python 3.10 的生产代码重构案例,分享如何优雅地解决这些问题。
1. 高效的关联键清洗(向量化操作)
在业务中,订单号在不同系统中可能略有差异。例如系统A是 10023.2.1(包含行号),而系统B中只有 10023。
为了进行匹配,必须先清洗主键。新手容易使用 for 循环或 apply 处理字符串,但在百万级数据下效率极低。推荐使用 Pandas 的 .str 访问器进行向量化处理。
# 场景:将 '12345.1.2' 清洗为 '12345'
# astype(str) 确保数据类型安全,防止数字类型报错
df_order['ORDER_NUMBER_CLEAN'] = df_order['ORDER_NUMBER'].astype(str).str.split('.').str[0]
这种写法比 df.apply(lambda x: x.split('.')[0]) 快得多,且代码更简洁。
2. 处理“3000年”问题:时间戳越界与日期比较
Pandas 默认的 pd.to_datetime 基于纳秒(nanosecond),其最大支持年份大约是 2262年。但在很多老旧 ERP 或数据库中,常用 3000-01-01 或 9999-12-31 来表示“无期限”或“无限大”。
如果直接强转日期,程序会报错。我们需要一种“防御性”的日期处理逻辑:
def calculate_status(row):
confirm_date = row['CONFIRM_DATE']
confirm_schedule_date = row['CONFIRM_SCHEDULE_DATE']
# 业务逻辑:如果日期有效且年份小于3000年,判定为某种状态
if pd.notna(confirm_date) and pd.notna(confirm_schedule_date):
try:
# 尝试直接获取 year 属性
# 注意:如果 Pandas 读取时没能解析成 Timestamp(因为越界),
# 这里可能是字符串或 Python 原生 datetime 对象
if confirm_schedule_date.year < 3000:
return "蓝色状态"
except AttributeError:
# 回退机制:如果不是 Timestamp 对象,尝试作为字符串切片处理
# 应对 Pandas 自动将越界日期识别为 object/str 的情况
try:
date_str = str(confirm_schedule_date)
if len(date_str) >= 4:
year = int(date_str[:4])
if year < 3000:
return "蓝色状态"
except:
pass # 忽略无法解析的脏数据
return "普通状态"
技巧:在 read_csv 时,可以使用 parse_dates 参数,但对于包含越界日期的列,Pandas 可能会默默将其保留为 object 类型(字符串)。编写混合逻辑(先尝试对象属性,再回退到字符串处理)能最大程度保证代码的健壮性。
3. 多条件复杂逻辑的行级运算
业务需求往往很变态,比如:“数量为0显示粉色,被挂起显示棕色,已确认显示红色,否则显示黑色”。
这种多条件的 if-elif-else 逻辑,最适合封装成函数,配合 apply(axis=1) 使用。
def calculate_dis_color(row):
# 优先级逻辑:从上到下,满足即返回
# 1. 数量检查
if pd.notna(row['QUANTITY']) and row['QUANTITY'] == 0:
return "粉色"
# 2. 挂起标志检查
if pd.notna(row['WIP_HOLD_FLAG']) and row['WIP_HOLD_FLAG'] == 'Y':
return "棕色"
# 3. 确认标志检查
if pd.notna(row['CONFIRM_FLAG']) and row['CONFIRM_FLAG'] == 'Y':
return "红色"
# 4. 默认
return "黑色"
# axis=1 表示逐行处理
df_dis['DIS_COLOR'] = df_dis.apply(calculate_dis_color, axis=1)
虽然 apply 在性能上不如向量化操作,但在逻辑过于复杂(涉及多列交叉判断)时,它是维护性最好的选择。
4. Merge 后的列名冲突处理 (_x, _y)
当两个 DataFrame 进行 merge 时,如果除了关联键(key)之外,还有其他列名相同(例如两个表中都有 ORDER_NUMBER),Pandas 会自动给左表的列加 _x 后缀,右表加 _y 后缀。
如果不处理,导出 Excel 时会让用户感到困惑。
# 关联操作:左键清洗后的号码,右键原始号码
df_merged = pd.merge(
df_order,
df_dis,
left_on=['ORDER_NUMBER_CLEAN', 'OLD_PARTNUMBER'],
right_on=['ORDER_NUMBER', 'OLD_PARTNUMBER'],
how='left'
)
# 陷阱:此时 df_merged 中可能同时存在 ORDER_NUMBER_x 和 ORDER_NUMBER_y
# ORDER_NUMBER_x 来自 df_order (主表)
# ORDER_NUMBER_y 来自 df_dis (附表)
# 最佳实践:明确指定输出列名,必要时重命名
output_columns = [
'ORDER_NUMBER_x', # 使用主表的原始订单号
'OLD_PARTNUMBER', # 关联键之一
'ORDER_NUMBER_CLEAN',# 清洗后的辅助列
'颜色匹配结果',
'DIS_COLOR'
]
# 导出前重命名,对用户更友好
df_final = df_merged[output_columns].rename(columns={'ORDER_NUMBER_x': '订单号'})
df_final.to_excel(OUTPUT_FILE, index=False)
总结
这个脚本虽然篇幅不长,但涵盖了数据清洗中几个典型的处理范式:
- 清洗优先:在 Merge 前统一数据格式(
split,strip)。 - 防御性编程:处理日期时考虑到越界和格式错误的情况。
- 逻辑封装:将复杂的
if-else抽离为函数,保持主流程清晰。 - 明确输出:显式选择需要的列 (
_xvs_y),避免输出混乱的中间数据。
使用 Python Pandas 处理数据,不仅仅是写出能跑的代码,更要写出能容忍脏数据、逻辑可维护的代码。
欢迎关注公众号"飞鸿影记(fhyblog)",探寻物件背后的逻辑,记录生活真实的影子。

作者:飞鸿影
出处:http://52fhy.cnblogs.com/
版权申明:没有标明转载或特殊申明均为作者原创。本文采用以下协议进行授权,自由转载 - 非商用 - 非衍生 - 保持署名 | Creative Commons BY-NC-ND 3.0,转载请注明作者及出处。


浙公网安备 33010602011771号