R语言高效统计时间窗口内同物料同工厂的工单请求次数
问题本质
你当前逐行循环实现的性能瓶颈根因非常明确:
- 每处理1行观测,都需要对全量700万行工单表做一次全表扫描过滤,整体时间复杂度为O(n²),按R的解释执行效率,确实会出现耗时按年计算的情况
- 循环过程中逐次往空dataframe里写入结果,会反复申请内存做数据拷贝,进一步放大性能损耗
- 循环内重复对日期字段做类型转换、对同维度字段做全量比对,没有预建索引、预排序的优化空间
- 原代码本身存在逻辑偏差:过滤条件中
(original_basic_start_date - wo.date_f < days.back)在满足“记录日期早于当前行日期”的前提下恒成立,实际并未实现30天回溯窗口的限制,会把所有历史上同物料同工厂的记录都统计进去
高效实现方案
方案1:data.table 实现(最适配千万行级规模的纯R计算场景)
data.table的分组、窗口计算逻辑是C层编译实现,没有R层面的循环开销,是当前R生态下大表计算效率最高的方案之一。
核心思路是先按「工厂+物料」分组,组内按日期预排序,再用自适应滚动窗口统计30天内的历史记录数,不需要重复扫描全表:
library(data.table) library(lubridate) # 转换为data.table格式,一次性完成日期类型转换 setDT(work.orders) work.orders[, original_basic_start_date := as_date(original_basic_start_date)] # 按分组维度、日期建排序索引,后续所有筛选计算都直接走索引 setkeyv(work.orders, c("plant", "material", "original_basic_start_date")) days_back <- 30 # 分组计算回溯窗口的请求次数 work.orders[, total_resb_found := { # 组内生成全1的计数向量,用来累加窗口内的记录数 count_vec <- rep(1, .N) # 对每一行,计算往前days_back天的窗口内累加值 frollapply( x = count_vec, width = function(idx) { # 当前行的日期值 curr_date <- .SD$original_basic_start_date[idx] # 统计满足:日期 >= 当前日期 - 30天 且 日期 < 当前日期 的记录数 sum(.SD$original_basic_start_date >= (curr_date - days_back) & .SD$original_basic_start_date < curr_date) }, FUN = sum, adaptive = TRUE ) }, by = .(plant, material)]
性能参考:700万行规模下,这套逻辑全量计算耗时通常在1-3分钟区间,相比原实现是百万级的效率提升。
方案2:DuckDB 下推计算(内存占用最低的方案)
如果你的内存不足以支撑全量dataframe的分组计算,可以用DuckDB的列式执行引擎把计算下推到数据库层,内存占用仅为原R内存计算的1/10左右,且会自动生成最优执行计划:
library(dplyr) library(duckdb) library(lubridate) # 建立本地内存数据库连接 con <- dbConnect(duckdb()) # 将数据写入数据库表,自动生成统计索引 work_orders_db <- copy_to(con, work.orders, name = "work_orders", overwrite = TRUE) days_back <- 30 # 直接写窗口计算逻辑,由数据库引擎执行 result <- work_orders_db %>% mutate(original_basic_start_date = as_date(original_basic_start_date)) %>% group_by(plant, material) %>% arrange(original_basic_start_date) %>% mutate( total_resb_found = sql(" COUNT(*) OVER ( PARTITION BY plant, material ORDER BY original_basic_start_date -- 统计当前行往前30天到当前行之间的记录数 RANGE BETWEEN INTERVAL 30 DAY PRECEDING AND CURRENT ROW -- 排除当前行本身,匹配原逻辑的日期小于当前行的要求 EXCLUDE CURRENT ROW ) ") ) %>% collect()
性能参考:700万行规模下,DuckDB的计算通常只需要几十秒,且不会因为数据规模扩大出现内存溢出的问题。
优化效果对比
- 原实现:每一行做全表扫描,时间复杂度O(n²),无预排序、无索引
- 优化后实现:分组后仅需对排序好的组内记录做窗口累加,时间复杂度O(n log n),全流程走预建索引
- 额外减少的开销:一次性完成日期类型转换,不在循环内重复做类型计算;直接在原表上追加计算列,不反复构造空dataframe做内存拷贝
内容的提问来源于stack exchange,提问作者user3639100
相关产品推荐
相关产品推荐

