Loading

Python Pandas 实战:复杂业务逻辑下的数据对账与清洗策略

在企业级数据处理中,我们经常面临“多源数据对账”的场景。比如:系统A是原始订单,系统B是调度或分发系统,我们需要核对两个系统中的订单状态、数量以及关键日期是否一致。

看似简单的 merge 操作,在实际落地时往往会遇到各种“坑”:

  1. 关联键格式不统一:有的带后缀,有的没带。
  2. 时间戳越界:遗留系统常用 9999-12-313000-01-01 表示“永久”,导致 Pandas 报 Out of bounds nanosecond timestamp
  3. 复杂的行级逻辑:状态判定不仅仅看某一列,而是涉及数量、标志位、日期等多列的组合逻辑。
  4. 合并后的列名冲突:分不清 _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-019999-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)

总结

这个脚本虽然篇幅不长,但涵盖了数据清洗中几个典型的处理范式:

  1. 清洗优先:在 Merge 前统一数据格式(split, strip)。
  2. 防御性编程:处理日期时考虑到越界和格式错误的情况。
  3. 逻辑封装:将复杂的 if-else 抽离为函数,保持主流程清晰。
  4. 明确输出:显式选择需要的列 (_x vs _y),避免输出混乱的中间数据。

使用 Python Pandas 处理数据,不仅仅是写出能跑的代码,更要写出能容忍脏数据、逻辑可维护的代码。

posted @ 2026-01-04 09:53  飞鸿影  阅读(7)  评论(0)    收藏  举报