PAYATHON 2026

如何根据后续退款记录删除 data.frame 中的相关行

支付老李

结论

先按日期和时间排序,再通过“客户邮箱 + 交易金额”查找退款对应的付款记录。处理退款时,删除退款行,以及该客户此前最近一笔尚未退款的同金额付款。

如果数据中有交易 ID,应优先按交易 ID 匹配。只有在缺少唯一标识时,才使用邮箱和金额。

Base R 实现

下面的代码按“后进先出”的方式匹配。同一客户有多笔同金额付款时,退款会匹配最近一笔尚未退款的付款。

# 合并日期和时间,并按交易发生顺序排列
df$datetime <- as.POSIXct(
  paste(df$Date, df$Time),
  format = "%Y.%m.%d %H:%M:%S",
  tz = "Asia/Shanghai"
)

df <- df[order(df$datetime), ]

# 统一邮箱格式,避免大小写和首尾空格影响匹配
normalize_email <- function(x) {
  tolower(trimws(x))
}

# 金额转换为整数分,减少浮点数比较带来的误差
amount_in_cents <- round(abs(df$Gross) * 100)

drop_row <- rep(FALSE, nrow(df))
pending_payments <- new.env(hash = TRUE, parent = emptyenv())

for (i in seq_len(nrow(df))) {
  if (df$Type[i] == "Website Payment") {
    key <- paste(
      normalize_email(df$Sender_email[i]),
      amount_in_cents[i],
      sep = "|"
    )

    previous <- if (exists(key, envir = pending_payments, inherits = FALSE)) {
      get(key, envir = pending_payments)
    } else {
      integer(0)
    }

    assign(key, c(previous, i), envir = pending_payments)
  }

  if (df$Type[i] == "Payment Refund") {
    key <- paste(
      normalize_email(df$Receiver_email[i]),
      amount_in_cents[i],
      sep = "|"
    )

    # 退款行本身需要删除
    drop_row[i] <- TRUE

    if (exists(key, envir = pending_payments, inherits = FALSE)) {
      candidates <- get(key, envir = pending_payments)

      if (length(candidates) > 0) {
        # 删除最近一笔尚未退款的付款
        payment_row <- tail(candidates, 1)
        drop_row[payment_row] <- TRUE

        candidates <- head(candidates, -1)

        if (length(candidates) > 0) {
          assign(key, candidates, envir = pending_payments)
        } else {
          rm(list = key, envir = pending_payments)
        }
      }
    }
  }
}

result <- df[!drop_row, ]

# 如果不需要辅助时间列
result$datetime <- NULL

result

处理后会保留三笔尚未退款的交易:

2018.07.12  12:45:13  Website Payment  30  aaa@customer.com
2018.07.24  07:10:00  Website Payment  30  bbb@customer.com
2018.08.17  15:17:40  Website Payment  30  ccc@custom.com

为什么不能只删除相邻行

退款记录不一定紧跟在付款记录后面,同一个客户也可能连续发生多笔金额相同的交易。因此,直接使用 lag() 或删除退款的上一行,只有在退款始终紧邻原付款时才可靠。

这段代码会记录所有尚未退款的付款。遇到退款后,它会从客户和金额均相同的候选记录中取最近一笔,因此不依赖付款行与退款行是否相邻。

注意邮箱拼写

示例中的付款邮箱是:

bbb@customer.com

退款记录中的收款邮箱是:

bbb@custom.com

两个字符串并不相同。只有当它们实际表示为同一个邮箱时,代码才能完成匹配。如果这是示例中的笔误,应先修正数据:

df$Receiver_email[
  df$Receiver_email == "bbb@custom.com"
] <- "bbb@customer.com"

不要用模糊匹配自动修正真实交易邮箱,否则可能会把退款关联到错误的客户。

更可靠的匹配条件

在实际业务中,邮箱和金额通常不足以唯一确定一笔交易。更稳妥的做法是使用支付平台提供的原始交易 ID,例如:

refund$Parent_transaction_id == payment$Transaction_id

如果没有交易 ID,至少还要结合币种、账户、订单号和退款发生时间。对于部分退款,也不能直接按金额是否相等来匹配,而要累计每笔原交易的已退款金额。

备注:内容仅供参考。