如何在 Pandas 中按分组标记首次满足条件的行

本文介绍如何基于分组(如客户名称)和布尔条件(如 `y/n` 列首次出现 `'y'`),为 dataframe 添加一列标记“首次满足条件的日期”,仅在对应行填充该日期,其余位置设为 `nan`。

在数据分析中,常需识别每个分组内首次满足特定条件的记录(例如:每位客户首次下单、首次响应“Y”、首次达标等),并将其关键字段(如日期)提取到新列中,其余行置空。Pandas 提供了高效向量化方法实现这一目标,无需循环或 apply。

核心思路分为三步:

  1. 构造基础布尔条件:筛选出所有 'Y/N' == 'Y' 的行;
  2. 按分组累计计数:对每组内满足条件的行进行累加计数,并标记“首次”(即累计值等于 1);
  3. 条件赋值:仅当某行同时满足 'Y' 且是本组首次时,才填入其 BAS_DT,否则为 NaN。

以下是完整实现代码:

import pandas as pd
import numpy as np

df = pd.DataFrame({
    'BAS_DT': ['2025-01-02', '2025-01-03', '2025-01-04', '2025-01-02', '2025-01-03'],
    'CUS_NAME': ['A', 'A', 'A', 'B', 'B'],
    'Y/N': ['Y', 'Y', 'Y', 'N', 'Y'],
    'cum_count': [1, 2, 3, 1, 2]
})

# 步骤 1:标记所有 'Y' 行
cond = df['Y/N'].eq('Y')

# 步骤 2:按 CUS_NAME 分组,对 cond 累计求和,并判断是否为组内首次(=1)
cond1 = cond.groupby(df['CUS_NAME']).cumsum().eq(1)

# 步骤 3:仅在同时满足 cond 和 cond1 的行填入 BAS_DT,否则为 NaN
df['occur_date'] = df['BAS_DT'].where(cond & cond1)

print(df)

输出结果与预期一致:

       BAS_DT CUS_NAME Y/N  cum_count  occur_date
0  2025-01-02        A   Y          1  2025-01-02
1  2025-01-03        A   Y          2         NaN
2  2025-01-04        A   Y          3         NaN
3  2025-01-02        B   N          1         NaN
4  2025-01-03        B   Y          2  2025-01-03

关键技巧说明

  • df['col'].eq(val) 比 df['col'] == val 更推荐,语义清晰且支持链式调用;
  • groupby(...).cumsum() 在布尔 Series 上会自动转为整数累加(True→1, False→0),天然适合统计“第几次满足条件”;
  • where(condition) 是向量化赋值的优雅方式,比 np.where 或 loc 更简洁安全。

⚠️ 注意事项

  • 若某组内无任何 'Y'(如客户 'C' 全为 'N'),则 occur_date 全为 NaN,符合逻辑;
  • 数据顺序影响结果——此方法依赖原始行序,确保 BAS_DT 已按时间升序排列(如示例所示),否则“首次”可能不具业务意义;
  • 如需获取每个客户的首次发生日期作为独立汇总表,可改用 df[cond].groupby('CUS_NAME')['BAS_DT'].first()。

该方法兼具性能、可读性与扩展性,适用于各类“分组首现标记”场景。