优化data.table中HR斜率与自定义累积和的高效无循环实现方案
Hey there! Let's fix those slow loop-based calculations for your large data.table. We'll use data.table's built-in fast grouping and rolling window functions to avoid loops entirely, which should give you a massive speedup.
First, let's recap your data and requirements
Your input data looks like this:
library(data.table) allData <- data.table(nightNo=c(1,1,1,1,1,1,2,2,2,2), withinNightNo=c(1,2,3,4,5,6,1,2,3,4), HR=c(1:10))
You need two new columns:
- HRSlope: Linear regression slope of
HRvswithinNightNousing the most recent 10 rows (or all prior rows if less than 10) within the samenightNo. - HRCumsum: Custom cumulative sum defined as
CUMSUMₙ = MAX(CUMSUMₙ₋₁, 0) + (HRₙ - MEAN(HR₁₋ₙ))
1. Calculating HRSlope Efficiently
Instead of looping, we can use rolling window sums to compute the necessary statistics for the slope formula. The slope of a linear regression between x (withinNightNo) and y (HR) can be calculated using:
slope = (n*sum(x*y) - sum(x)*sum(y)) / (n*sum(x²) - sum(x)²)
Where n is the number of observations in the window.
We'll use frollsum from data.table to compute rolling sums for each nightNo with a window size of 10 (aligned to the right):
# Calculate rolling sums for each nightNo, window=10, align=right allData[, `:=`( sumX = frollsum(withinNightNo, n=10, align="right", na.rm=FALSE), sumY = frollsum(HR, n=10, align="right", na.rm=FALSE), sumXY = frollsum(withinNightNo*HR, n=10, align="right", na.rm=FALSE), sumX2 = frollsum(withinNightNo^2, n=10, align="right", na.rm=FALSE), n_win = frollsum(rep(1, .N), n=10, align="right", na.rm=FALSE) ), by = nightNo] # Compute slope, set to NaN when n_win < 2 (can't compute slope with <2 points) allData[, HRSlope := ifelse(n_win < 2, NaN, (n_win*sumXY - sumX*sumY) / (n_win*sumX2 - sumX^2))] # Clean up temporary columns allData[, c("sumX", "sumY", "sumXY", "sumX2", "n_win") := NULL]
2. Calculating HRCumsum Efficiently
The custom cumulative sum has two parts:
- First, compute
HRₙ - mean(HR₁₋ₙ)for each row within anightNo - Then apply the recursive rule:
CUMSUMₙ = max(CUMSUMₙ₋₁, 0) + current_part
We can compute this using grouped cumulative sums and a recursive calculation with Reduce (vectorized within each group):
# First compute the (HR - mean(1..n)) part for each nightNo allData[, hr_part := HR - cumsum(HR)/seq_len(.N), by = nightNo] # Apply the recursive max + sum rule using Reduce within each group allData[, HRCumsum := Reduce(function(prev, curr) max(prev, 0) + curr, hr_part, accumulate = TRUE), by = nightNo] # Clean up temporary column allData[, hr_part := NULL]
Full Code & Verification
Putting it all together, here's the complete script:
library(data.table) # Sample data allData <- data.table(nightNo=c(1,1,1,1,1,1,2,2,2,2), withinNightNo=c(1,2,3,4,5,6,1,2,3,4), HR=c(1:10)) # Calculate HRSlope allData[, `:=`( sumX = frollsum(withinNightNo, n=10, align="right", na.rm=FALSE), sumY = frollsum(HR, n=10, align="right", na.rm=FALSE), sumXY = frollsum(withinNightNo*HR, n=10, align="right", na.rm=FALSE), sumX2 = frollsum(withinNightNo^2, n=10, align="right", na.rm=FALSE), n_win = frollsum(rep(1, .N), n=10, align="right", na.rm=FALSE) ), by = nightNo] allData[, HRSlope := ifelse(n_win < 2, NaN, (n_win*sumXY - sumX*sumY) / (n_win*sumX2 - sumX^2))] allData[, c("sumX", "sumY", "sumXY", "sumX2", "n_win") := NULL] # Calculate HRCumsum allData[, hr_part := HR - cumsum(HR)/seq_len(.N), by = nightNo] allData[, HRCumsum := Reduce(function(prev, curr) max(prev, 0) + curr, hr_part, accumulate = TRUE), by = nightNo] allData[, hr_part := NULL] # View the result print(allData)
Running this will give you exactly the expected output:
nightNo withinNightNo HR HRSlope HRCumsum 1: 1 1 1 NaN 0.0 2: 1 2 2 1 0.5 3: 1 3 3 1 1.5 4: 1 4 4 1 3.0 5: 1 5 5 1 5.0 6: 1 6 6 1 7.5 7: 2 1 7 NaN 0.0 8: 2 2 8 1 0.5 9: 2 3 9 1 1.5 10: 2 4 10 1 3.0
Key Efficiency Notes
- HRSlope: Uses
frollsumwhich is implemented in C, so it's orders of magnitude faster than loops. The window size of 10 ensures we only look at the most recent 10 rows per night. - HRCumsum: Avoids double loops by first computing the mean part with vectorized cumulative sums, then using
Reducewithin each group (which is still vectorized and much faster than row-wise loops).
内容的提问来源于stack exchange,提问作者Henk

