PAYATHON 2026

使用 mutate 标记其他行中同时出现多个匹配值

支付小周

结论

先按 date 和 merchant 分组,再检查每个分组内是否有金额互为相反数的交易:

library(dplyr)

transactions %>%
  group_by(date, merchant) %>%
  mutate(refund = (-amount) %in% amount) %>%
  ungroup()

分组后,日期、商户和相反金额必须同时匹配,因此不会把其他日期或商户的交易误判为退款。

原写法的问题

原来的几个判断相互独立:

date %in% date
merchant %in% merchant
(-amount) %in% amount

前两个条件始终为 TRUE,因为每个日期和商户都存在于自身所在的列中。即使换成与其他数据比较,多个独立的 %in% 也只能确认各个值分别存在,无法保证它们来自同一行。

需要判断的是:

在同一天、同一商户的交易中,当前金额的相反数是否存在。

group_by(date, merchant) 将检查范围限定在了对应的日期和商户内。

完整示例

library(dplyr)
library(tibble)

transactions <- tibble(
  date = as.Date(c(
    "2020-01-01",
    "2020-01-02",
    "2020-01-02",
    "2020-01-03",
    "2020-01-04"
  )),
  merchant = c("A", "B", "B", "C", "A"),
  amount = c(10, 5, -5, 9, 10)
)

result <- transactions %>%
  group_by(date, merchant) %>%
  mutate(refund = (-amount) %in% amount) %>%
  ungroup()

result

输出结果:

# A tibble: 5 × 4
  date       merchant amount refund
  <date>     <chr>      <dbl> <lgl>
1 2020-01-01 A             10 FALSE
2 2020-01-02 B              5 TRUE
3 2020-01-02 B             -5 TRUE
4 2020-01-03 C              9 FALSE
5 2020-01-04 A             10 FALSE

商户 B 在 2020-01-02 的分组中有 5 和 -5 两笔交易,金额互为相反数,所以两行都会标记为 TRUE。

允许退款晚于付款

如果正数代表付款,负数代表退款,并且退款可以发生在付款当天或之后,可以先为每行添加唯一标识,再执行自连接:

transactions_with_id <- transactions %>%
  mutate(transaction_id = row_number())

matched_ids <- transactions_with_id %>%
  filter(amount > 0) %>%
  inner_join(
    transactions_with_id %>%
      filter(amount < 0),
    by = join_by(
      merchant,
      amount == -amount,
      date <= date
    ),
    suffix = c("_payment", "_refund")
  ) %>%
  transmute(
    ids = Map(
      c,
      transaction_id_payment,
      transaction_id_refund
    )
  ) %>%
  tidyr::unnest_longer(ids) %>%
  distinct(ids)

result <- transactions_with_id %>%
  mutate(refund = transaction_id %in% matched_ids$ids) %>%
  select(-transaction_id)

这段代码基于以下条件:

  • 正数表示付款;
  • 负数表示退回款项;
  • 付款和退款的绝对金额完全相等;
  • 退款日期不早于付款日期;
  • merchant 相同。

join_by() 的非等值连接语法需要相应版本的 dplyr。如果只需处理当天发生的退款,前面的分组写法更简单。

注意事项

假设同一分组中有两笔 5 和一笔 -5,简单使用 %in% 会将三笔交易全部标记为 TRUE。它只判断相反金额是否存在,不会进行一对一配对。

如果每笔退款只能对应一笔付款,就需要根据交易时间、交易流水号或出现顺序制定明确的配对规则。

金额为 0 时,-amount 仍为 0,因此单独一笔零金额交易也会与自身匹配。可以增加条件将其排除:

transactions %>%
  group_by(date, merchant) %>%
  mutate(refund = amount != 0 & (-amount) %in% amount) %>%
  ungroup()

备注:内容仅供参考。