PAYATHON 2026

使用 dplyr 识别订阅交易中的用户套餐切换行为

数据小许

结论

先按 user_id 分组,并按交易日期排序。然后以每个用户的首单套餐为判断基准:

  • 首单是 "monthly" 时,第一次切换到 "3-months" 或 "6-months" 的订单标记为 "upgrade"。
  • 首单是 "3-months" 或 "6-months" 时,第一次切换到 "monthly" 的订单标记为 "downgrade"。
  • 同一用户之后继续使用该套餐时,不再重复标记。

实现时可以先找出所有候选行为,再通过累计计数只保留第一次出现的行为。

dplyr 实现

library(dplyr)

df_output <- df %>%
  mutate(
    .txn_date = lubridate::mdy(date)
  ) %>%
  arrange(user_id, .txn_date) %>%
  group_by(user_id) %>%
  mutate(
    .initial_plan = first(plan),

    .candidate = case_when(
      row_number() > 1L &&
        .initial_plan == "monthly" &&
        plan %in% c("3-months", "6-months") &&
        plan != lag(plan) ~ "upgrade",

      row_number() > 1L &&
        .initial_plan %in% c("3-months", "6-months") &&
        plan == "monthly" &&
        plan != lag(plan) ~ "downgrade",

      TRUE ~ "-"
    ),

    behavior_type = if_else(
      .candidate != "-" & cumsum(.candidate != "-") == 1L,
      .candidate,
      "-"
    )
  ) %>%
  ungroup() %>%
  select(-.txn_date, -.initial_plan, -.candidate)

生成的 behavior_type 与题目中的预期结果一致:用户 1 和用户 3 的第一笔长期套餐订单会标记为 "upgrade",用户 4 的第一笔月度套餐订单会标记为 "downgrade",其他记录均为 "-"。

判断逻辑

数据按时间排好后,.initial_plan = first(plan) 会取得每个用户的首单套餐。

plan != lag(plan) 用于确认当前套餐与上一笔订单不同。比如用户已经从 "monthly" 切换到 "3-months",下一笔订单仍是 "3-months",这笔订单就不算新的切换。

.candidate 会先标记所有符合业务定义的候选订单。下面的条件则只保留第一个候选行为:

cumsum(.candidate != "-") == 1L

第二次及以后出现的候选行为都会改为 "-"。

如果 date 已经是 Date 类型

如果 date 已经是 Date 或可靠的日期时间类型,可以省略 mdy():

df_output <- df %>%
  arrange(user_id, date) %>%
  group_by(user_id) %>%
  mutate(
    .initial_plan = first(plan),

    .candidate = case_when(
      row_number() > 1L &&
        .initial_plan == "monthly" &&
        plan %in% c("3-months", "6-months") &&
        plan != lag(plan) ~ "upgrade",

      row_number() > 1L &&
        .initial_plan %in% c("3-months", "6-months") &&
        plan == "monthly" &&
        plan != lag(plan) ~ "downgrade",

      TRUE ~ "-"
    ),

    behavior_type = if_else(
      .candidate != "-" & cumsum(.candidate != "-") == 1L,
      .candidate,
      "-"
    )
  ) %>%
  ungroup() %>%
  select(-.initial_plan, -.candidate)

注意事项

示例日期使用月/日/年格式,因此代码调用了 lubridate::mdy()。如果实际数据采用日/月/年格式,需要改用 lubridate::dmy()。

这段代码假设每个用户按时间排序后的第一笔交易就是 "acquisition"。如果数据可能缺少首单,或者同一用户有多条 "acquisition",建议先检查:

df %>%
  count(user_id, order_type) %>%
  filter(order_type == "acquisition", n != 1L)

如果同一天可能有多笔交易,只按日期无法判断真实顺序。此时应在 arrange() 中加入订单时间或订单编号,例如:

arrange(user_id, transaction_time, order_id)

这里的升级和降级是相对于首单套餐定义的,并非对所有相邻套餐变化进行分类。例如,从 "3-months" 切换到 "6-months" 不会标记为升级,因为题目只把首单为 "monthly" 后切换到长期套餐定义为升级。

备注:内容仅供参考。