如何根据后续退款记录删除 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,至少还要结合币种、账户、订单号和退款发生时间。对于部分退款,也不能直接按金额是否相等来匹配,而要累计每笔原交易的已退款金额。
备注:内容仅供参考。
