在利用Python做数据分析时,我们经常会拿到一个pandas的DataFrame,并希望按照某种“相对范围”而不是固定的绝对区间来做聚合。所谓相对范围,通常是指以当前行的值为基准,向前或向后取一定的偏移量、行数或数值跨度,再把落在这个范围内的记录合并起来计算总和、均值等指标。这种需求在用固定分箱或resample很难直接满足,需要结合滑动窗口与自定义分组技巧。

什么是相对范围值聚合
相对范围值聚合指的是聚合窗口的边界由每条记录自身的值或位置决定,而不是由全局统一的切分点决定。例如,有一列数值表示事件发生点的距离,我们希望以每条记录为起点,把距离在加五以内的后续记录都算作同一组进行求和。这与rolling固定行数窗口不同,因为rolling的窗口大小固定,而相对范围可能让某些组的成员数量差异很大。
从底层逻辑看,相对范围聚合往往要先计算出一个“组标号”,再按组标号调用groupby做聚合。组标号可以通过累计条件生成:当当前值与上一条已分组的值差值超过阈值时,组号加一。这样就把连续的近邻数据划分成了多个相对簇,后续聚合就变得非常简单且高效。
使用shift与cumsum构建相对分组
最直观的实现方式是借助shift比较相邻行的差值,再用cumsum生成分组键。下面示例展示如何按数值列val,将相差不超过3的连续行分为一组,并计算每组的平均值与条数。
import pandas as pd
data = {'val': [1, 2, 5, 6, 10, 11, 20]}
df = pd.DataFrame(data)
# 计算与前一行的差值是否超过阈值
threshold = 3
gap = df['val'] - df['val'].shift(1)
# 超过阈值的位置记为新组起点
new_group = gap > threshold
# 累积求和得到组编号
df['group'] = new_group.cumsum()
result = df.groupby('group')['val'].agg(['mean', 'count'])
print(result)
上面的代码先通过shift(1)拿到前一条记录的val,相减后判断间隔。一旦间隔大于3,就说明应该开启一个新组。cumsum把布尔值转成递增整数,形成分组键。最后groupby一次性完成聚合,避免了手写循环,也方便扩展到多列。
这种写法的优点是逻辑清晰、易于修改阈值,并且能自然处理不规则间隔的数据。缺点是它只能处理“向前看”的连续片段,如果相对范围需要包含前后行,就要改用双向判断或基于索引的偏移合并。
基于行位置的滑动相对聚合
有时相对范围不是数值差,而是“以当前行为中心,前后各取两行”这类基于位置的窗口。虽然rolling能实现固定窗口,但若要按分组做不重叠的相对块聚合,可以结合floor除法构造块号。
import pandas as pd
df = pd.DataFrame({'score': [10, 20, 30, 40, 50, 60, 70, 80]})
# 每3行作为一个相对范围块
block_size = 3
df['block'] = df.index // block_size
agg = df.groupby('block')['score'].sum()
print(agg)
这里用整数除法把行索引映射成块编号,相当于以每三行为一个相对范围做聚合。它适合数据已经按时间或顺序排好、且希望均匀切片的场景。若数据存在缺失索引,应改用reset_index再计算块号,防止分组错乱。
需要注意的是,当相对范围依赖复杂条件(如时间差混合数值差)时,可以先用apply逐组判断,但apply性能较弱。更好的做法是用向量化比较生成掩码,再走cumsum路线,既保留灵活性也维持速度。
相对范围聚合的常见误区
一个容易混淆的概念是把rolling当作相对范围聚合的万能解。实际上rolling的窗口是“绝对大小”,例如rolling(3)永远看前三条,而不会根据值是否接近动态调整。如果你的业务说“把差值小于某值的放一起”,rolling无法直接表达,必须回到分组思路。
避坑要点:先问清楚范围是按值偏移还是按行偏移,再决定用groupby加cumsum还是rolling,混用会导致统计结果 silently 错误。
另一个误区是在生成组号时忘了处理首行NaN。shift(1)的第一行是NaN,相减后为NaN,比较运算会得到False,cumsum会把它当0,通常正好作为第一组,但如果你用了notna过滤就可能丢首组。建议显式fillna(False)来明确意图。
总结与实践建议
基于DataFrame相对范围值进行Python聚合,核心在于把“相对”翻译成分组键。数值相对用差值加cumsum,位置相对用索引整除,复杂相对可组合多个掩码。相比纯循环,向量化分组既易读又高效。
在实际项目中,建议先在小样本上打印分组键确认边界是否符合预期,再套用agg。遇到超大DataFrame时,注意cumsum与shift均为O(n)操作,整体仍远快于Python层循环,可放心使用。