Pandas滚动窗口分析:从数据平滑到特征工程实战
1. 从“看单点”到“看趋势”:为什么我们需要滑动窗口
如果你处理过时间序列数据,比如股票价格、传感器读数、或者网站每天的访问量,你肯定遇到过这样的需求:想知道过去7天的平均销售额,或者计算最近5分钟内的温度移动平均值。这时候,如果你还在用循环去一个个切片、计算,那效率就太低了,代码也会变得臃肿不堪。
pandas 库里的 rolling() 函数,就是专门为解决这类“滑动窗口计算”而生的利器。它的核心思想非常直观:想象一个固定宽度的“窗口”,沿着你的数据(通常是按时间或索引顺序排列的)一步步滑动。在每一个停留的位置,窗口都框住一部分数据,然后对这个窗口内的数据进行某种聚合计算(比如求和、求平均、求标准差)。这个计算的结果,就作为新序列中对应位置的值。
举个例子,你有一列每日销售额 [100, 200, 150, 300, 250] ,你想计算3天的移动平均。那么 rolling(window=3) 就会创建宽度为3的窗口:
- 窗口覆盖第1、2、3天时,计算
(100+200+150)/3 = 150,这个值放在结果序列的第3天位置。 - 窗口滑动到覆盖第2、3、4天时,计算
(200+150+300)/3 ≈ 216.67,放在第4天位置。 - 窗口继续滑动……
最终你得到的是一个平滑后的序列,能更好地反映趋势,而不是被某一天的异常值所干扰。这比单纯看每一天的孤立数字要有用得多。无论是金融领域的技术指标(如布林带、MACD),还是物联网中的异常检测,或是业务分析中的周期对比,滑动窗口都是基础中的基础。接下来,我会带你从最基础的用法开始,把 rolling() 这个工具彻底搞明白,让你在数据处理时能信手拈来。
2. 核心参数拆解:window、min_periods与center
要玩转 rolling() ,首先得理解它的几个核心参数。这些参数决定了窗口如何滑动、如何计算,是精准控制计算行为的关键。
2.1 window:窗口的大小与灵活性
window 参数是最核心的,它定义了窗口的大小。它可以是整数,也可以是偏移量字符串,这给了我们很大的灵活性。
整数窗口 :这是最常用的形式,表示窗口包含固定数量的观测值。
import pandas as pd
import numpy as np
# 创建一个简单的Series
s = pd.Series([1, 2, 3, 4, 5])
# 窗口大小为3
rolling_obj = s.rolling(window=3)
print(rolling_obj.mean())
输出会是:
0 NaN
1 NaN
2 2.0
3 3.0
4 4.0
dtype: float64
注意到前两个位置(索引0和1)的结果是 NaN 。这是因为当窗口起始位置在数据开头时,无法凑满3个值。这是默认行为,后面我们会用 min_periods 来控制它。
偏移量窗口(针对时间序列) :当你的数据索引是时间类型( DatetimeIndex , TimedeltaIndex 等)时, window 可以用字符串指定一个时间周期,比如 ‘3D’ (3天)、 ‘2H’ (2小时)、 ‘30T’ (30分钟)。这时,窗口大小不再是固定的行数,而是固定的时间跨度。 pandas 会自动根据索引的时间间隔来动态决定每个窗口包含哪些数据点,这对于不规则时间序列(比如股票tick数据)特别有用。
# 创建一个带有时间索引的Series
date_rng = pd.date_range(start='2023-01-01', end='2023-01-10', freq='D')
ts = pd.Series(range(10), index=date_rng)
# 计算3天的移动平均
rolling_mean = ts.rolling(window='3D').mean()
print(rolling_mean.head())
在这个例子中,每个窗口都会包含索引时间在窗口开始时间点之后3天内的所有数据行。
2.2 min_periods:处理窗口初期的计算规则
min_periods 参数解决了我们上面看到的 NaN 问题。它定义了窗口内至少需要有多少个非 NaN 值,才进行计算并输出结果;否则,结果就是 NaN 。它的默认值等于 window 参数。
- 默认情况 :
min_periods = window。这意味着窗口必须被完全填满(即窗口内所有位置都有数据)才进行计算。这就是为什么在序列开头会有NaN。 - 自定义
min_periods:你可以将其设置为一个更小的数。例如,rolling(window=3, min_periods=1)。这意味着只要窗口内有至少1个值,就进行计算。
输出:s = pd.Series([1, 2, 3, 4, 5]) print(s.rolling(window=3, min_periods=1).mean())
这样,序列开头也能得到有意义的计算结果,非常适合你希望从第一个数据点就开始观察趋势的场景。0 1.0 # 窗口[1],有1个值,平均值是1 1 1.5 # 窗口[1,2],平均值(1+2)/2=1.5 2 2.0 # 窗口[1,2,3],平均值2.0 3 3.0 # 窗口[2,3,4],平均值3.0 4 4.0 # 窗口[3,4,5],平均值4.0 dtype: float64
注意 :
min_periods的设置需要权衡。设为1虽然能获得更多结果,但在窗口初期,由于数据点少,计算出的统计量(如标准差)可能波动很大,不够稳定。根据你的分析目的选择合适的值。
2.3 center:结果的对齐方式
center 参数决定了计算出的结果值应该对齐到窗口的哪个位置。默认是 False 。
center=False(默认):结果对齐到窗口的 右边缘 。也就是说,用窗口内数据计算出的值,会被放在这组数据中 最后一个 数据点所在的位置。我们之前的例子都是这种模式。这很符合直觉:“过去3天的平均值”这个标签,理应贴在第3天上。center=True:结果对齐到窗口的 中心 。计算出的值会被放在窗口覆盖范围的中间位置。这对于平滑数据、进行可视化尤其有用,可以避免相位滞后。
输出:s = pd.Series([1, 2, 3, 4, 5, 6]) print(“右对齐 (center=False):”) print(s.rolling(window=3).mean()) print(“\n中心对齐 (center=True):”) print(s.rolling(window=3, center=True).mean())
可以看到,中心对齐后,序列两端的值变成了右对齐 (center=False): 0 NaN 1 NaN 2 2.0 # 对应窗口[1,2,3] 3 3.0 # 对应窗口[2,3,4] 4 4.0 # 对应窗口[3,4,5] 5 5.0 # 对应窗口[4,5,6] dtype: float64 中心对齐 (center=True): 0 NaN 1 2.0 # 对应窗口[0,1,2] -> 中心是索引1 2 3.0 # 对应窗口[1,2,3] -> 中心是索引2 3 4.0 # 对应窗口[2,3,4] -> 中心是索引3 4 5.0 # 对应窗口[3,4,5] -> 中心是索引4 5 NaN dtype: float64NaN,因为中心对齐的窗口在两端无法完整居中。但中间部分的结果值的位置发生了偏移,在平滑曲线时,这个曲线不会“落后”于原始数据。
3. 不只是求平均:丰富的内置聚合函数
调用 rolling() 会返回一个 Rolling 对象。这个对象本身并不计算,它像一个“计算蓝图”。你需要在这个对象上调用聚合方法,才能真正触发计算。 pandas 提供了几乎所有常见的统计函数。
3.1 基础统计函数
这些是最常用的,名字一看就懂:
.mean(): 平均值.sum(): 求和.std(): 标准差(默认分母是 n-1,即样本标准差).var(): 方差.min(): 最小值.max(): 最大值.median(): 中位数.quantile(q): 分位数(需传入参数q,如0.5代表中位数)
df = pd.DataFrame({‘A’: [1, 2, 3, 4, 5],
‘B’: [10, 20, 30, 40, 50]})
# 计算窗口为3的各种统计量
print(“移动和:\n”, df[‘A’].rolling(3).sum())
print(“\n移动标准差:\n”, df[‘A’].rolling(3).std())
print(“\n移动中位数:\n”, df[‘A’].rolling(3).median())
3.2 进阶与计数函数
.count(): 计算窗口内非NaN值的数量。这个函数不受min_periods影响,它总是返回实际的数量。.cov(other)/.corr(other): 计算当前序列与另一个序列 (other) 在滚动窗口内的协方差或相关系数。这在分析两个时间序列的联动关系时非常有用。.apply(func, raw=False): 万能方法,允许你传入一个自定义函数func来对窗口数据进行操作。raw=True时,func接收一个numpy数组;raw=False时,接收一个pandas Series。
3.3 在DataFrame上的应用
rolling() 可以应用于整个 DataFrame ,此时会对每一列 独立 进行滑动窗口计算。
df = pd.DataFrame(np.random.randn(10, 3), columns=[‘X’, ‘Y’, ‘Z’])
# 计算每列过去3行的移动平均
df_rolling_mean = df.rolling(window=3).mean()
print(df_rolling_mean.head())
这非常方便,一次操作就能得到所有指标的平滑序列。
实操心得 :虽然可以对整个
DataFrame应用rolling(),但有时我们只关心某几列。直接对DataFrame切片后的列进行操作(如df[[‘col1‘, ‘col2‘]].rolling(5).mean())在性能上通常更好,代码意图也更清晰。
4. 实战演练:从数据清洗到特征工程
理解了基本用法,我们来看看 rolling() 在实际数据分析流水线中能扮演什么角色。它绝不仅仅是求个移动平均那么简单。
4.1 场景一:平滑数据与去除噪声
这是最经典的应用。传感器数据、用户活跃度、股价等都含有噪声。一个适中的移动平均可以有效地平滑曲线,让我们看清主要趋势。
# 假设我们有一组带有噪声的模拟温度数据
np.random.seed(42)
time = pd.date_range(‘2023-06-01’, periods=100, freq=‘H’)
temperature = 20 + 5 * np.sin(2 * np.pi * np.arange(100) / 24) + np.random.normal(0, 1, 100)
ts_temp = pd.Series(temperature, index=time)
# 计算6小时移动平均(平滑短期波动)
ts_smoothed = ts_temp.rolling(window=‘6H’, min_periods=1).mean()
# 可视化对比
import matplotlib.pyplot as plt
plt.figure(figsize=(12, 6))
plt.plot(ts_temp.index, ts_temp, label=‘原始数据 (带噪声)’, alpha=0.6)
plt.plot(ts_smoothed.index, ts_smoothed, label=‘6小时移动平均’, linewidth=2, color=‘red’)
plt.legend()
plt.title(‘滑动窗口平滑数据示例’)
plt.show()
通过调整 window 大小,你可以控制平滑的程度。窗口越大,曲线越平滑,但对快速变化的反应也越迟钝。
4.2 场景二:计算滚动统计量作为新特征
在机器学习中,我们经常需要基于历史行为创造新的特征。 rolling() 是特征工程的利器。
# 模拟用户每日交易金额
dates = pd.date_range(‘2023-01-01’, ‘2023-01-31’, freq=‘D’)
np.random.seed(123)
transactions = np.random.randint(50, 500, size=len(dates))
df_user = pd.DataFrame({‘date’: dates, ‘amount’: transactions}).set_index(‘date’)
# 创建滚动特征
df_user[‘rolling_3d_mean’] = df_user[‘amount’].rolling(window=3, min_periods=1).mean()
df_user[‘rolling_7d_std’] = df_user[‘amount’].rolling(window=7, min_periods=1).std() # 波动性
df_user[‘rolling_7d_sum’] = df_user[‘amount’].rolling(window=7, min_periods=1).sum() # 周累计
df_user[‘amount_ratio_to_7d_avg’] = df_user[‘amount’] / df_user[‘rolling_7d_mean’] # 与近期均值的比值
print(df_user.head(10))
这样,我们就为每一天的数据,都附上了“近期平均消费水平”、“消费波动情况”、“本周累计消费”等动态特征,这些特征往往比单纯的当日值更有预测力。
4.3 场景三:检测异常值
通过计算滚动均值与标准差,我们可以定义数据的“正常范围”,并将超出此范围的点视为潜在异常。
# 继续使用上面的温度数据
ts_temp = ts_temp.copy() # 避免修改原数据
# 计算滚动统计:过去24小时的均值和标准差
rolling_mean = ts_temp.rolling(window=‘24H’, min_periods=12).mean()
rolling_std = ts_temp.rolling(window=‘24H’, min_periods=12).std()
# 定义异常:偏离滚动均值超过3个滚动标准差
threshold = 3
ts_temp[‘upper_bound’] = rolling_mean + threshold * rolling_std
ts_temp[‘lower_bound’] = rolling_mean - threshold * rolling_std
ts_temp[‘is_outlier’] = (ts_temp[‘temperature’] > ts_temp[‘upper_bound’]) | (ts_temp[‘temperature’] < ts_temp[‘lower_bound’])
# 查看异常点
outliers = ts_temp[ts_temp[‘is_outlier’]]
print(f”检测到 {len(outliers)} 个潜在异常点”)
print(outliers[[‘temperature’, ‘upper_bound’, ‘lower_bound’]].head())
这种方法比用全局均值和标准差更合理,因为它考虑了数据的局部特征,能适应趋势和季节性的变化。
5. 避坑指南与性能优化
rolling() 用起来顺手,但也有一些细节不注意就会踩坑。下面是我在实际项目中总结的几个关键点。
5.1 缺失值(NaN)的连锁反应
rolling() 函数在计算时,默认会忽略窗口内的 NaN 值。但是, NaN 的存在会影响窗口内有效数据的数量,进而可能因为不满足 min_periods 而导致输出结果也为 NaN 。
常见坑点 :你的原始数据中间有缺失值,你用 rolling(window=5).mean() 想计算移动平均。在缺失值附近,即使时间窗口跨度够,但因为有效数据点不足5个,结果还是会变成 NaN ,这个 NaN 会随着计算向后传播。
s = pd.Series([1, 2, np.nan, 4, 5, 6, 7])
print(s.rolling(window=3).mean())
输出:
0 NaN
1 NaN
2 NaN # 窗口[2, NaN, 4],有效点不足3个(默认min_periods=3)
3 NaN # 窗口[NaN, 4, 5],有效点不足3个
4 5.0 # 窗口[4, 5, 6],平均值5.0
5 6.0 # 窗口[5, 6, 7],平均值6.0
dtype: float64
解决方案 :
- 数据预处理 :在应用
rolling()之前,先处理缺失值。根据业务场景,可以用前向填充(.ffill())、后向填充(.bfill())或插值(.interpolate())等方法。 - 调整
min_periods:如果你能接受在数据缺失时使用更少的数据点进行计算,可以设置min_periods=1。但需要明白这可能会在数据稀疏处引入噪声。 - 使用
.apply()自定义 :在自定义函数里处理NaN。
5.2 时间序列索引与频率陷阱
当使用偏移量字符串(如 ‘3D’ )作为 window 时,前提是你的数据索引必须是时间类型,并且最好是 规则频率 的。如果索引不规则, pandas 仍然会计算,但你需要理解其行为:它会根据每个时间点,回溯 window 指定的时间长度,将落在这个时间区间内的所有点纳入窗口。这可能导致不同窗口包含的数据点数量差异很大。
建议 :对于不规则时间序列,使用偏移量窗口是合适的。但对于规则时间序列,使用整数窗口( window=3 )和偏移量窗口( window=‘3D’ )在数据点均匀的情况下结果一致,但整数窗口通常计算更快。
5.3 性能考量:向量化操作与避免循环
rolling().mean() , rolling().sum() 等内置聚合函数是高度优化的向量化操作,速度非常快。 绝对不要 自己写循环去实现滑动窗口计算。
性能瓶颈往往出现在使用 .apply() 时 。 .apply(func) 会对每个窗口调用一次Python函数,如果数据量很大(几十万、百万行),这会非常慢。
# 慢:使用apply计算自定义的加权平均
def weighted_mean(series):
weights = np.arange(1, len(series)+1) # 线性权重
return np.average(series, weights=weights)
# 对于大数据量,这行会很慢
result_slow = large_series.rolling(10).apply(weighted_mean, raw=False)
优化策略 :
- 优先使用内置函数 :几乎所有常见统计都有内置函数,它们底层是C/C++/Cython实现的。
- 如果必须用
.apply():- 设置
raw=True。这会将窗口数据作为numpy数组传递给函数,避免了pandas Series的构造开销,通常能快不少。 - 审视你的自定义函数,看能否用
numpy的向量化函数重写。
- 设置
- 对于超大数据 :考虑使用更专业的库如
numba来加速,或者将数据分块处理。
5.4 窗口类型扩展:expanding() 与 ewm()
rolling() 的兄弟函数也值得了解,它们解决了稍有不同的需求。
-
expanding():扩展窗口。窗口从时间序列的起点开始,一直扩展到当前行。它计算的是“截至当前”的所有历史数据的聚合值(如累计和、累计平均)。df.expanding().mean()就等价于df.cumsum() / (np.arange(len(df)) + 1)。s = pd.Series([1, 2, 3, 4, 5]) print(s.expanding().mean()) # 输出:1.0, 1.5, 2.0, 2.5, 3.0 -
.ewm():指数加权移动。它给窗口内的数据赋予指数衰减的权重,越近的数据权重越高。它不像rolling()有一个清晰的窗口边界,而是用一个衰减因子(alpha或span、halflife)来控制“记忆”的长度。ewm对最近的变化更敏感,常用于金融分析。# span参数大致相当于平均窗口大小 ewm_mean = s.ewm(span=3, adjust=False).mean() print(ewm_mean)选择
rolling还是ewm,取决于你是想要一个绝对公平的近期窗口(rolling),还是认为近期的数据理应拥有更大的话语权(ewm)。
6. 综合案例:分析股票价格波动
让我们用一个更综合的例子,把前面讲的知识点串起来。假设我们有一支股票的日度收盘价数据,我们想计算几个常见的技术指标。
# 1. 准备数据(这里用模拟数据)
np.random.seed(2024)
dates = pd.date_range(‘2023-01-01’, ‘2023-12-31’, freq=‘B’) # 工作日
price = 100 + np.cumsum(np.random.randn(len(dates)) * 0.5) # 随机游走模拟股价
df_stock = pd.DataFrame({‘Close’: price}, index=dates)
# 2. 计算简单移动平均线 (SMA)
df_stock[‘SMA_10’] = df_stock[‘Close’].rolling(window=10, min_periods=5).mean()
df_stock[‘SMA_30’] = df_stock[‘Close’].rolling(window=30, min_periods=15).mean()
# 3. 计算布林带 (Bollinger Bands)
window_bb = 20
df_stock[‘SMA_20’] = df_stock[‘Close’].rolling(window=window_bb).mean()
df_stock[‘STD_20’] = df_stock[‘Close’].rolling(window=window_bb).std()
df_stock[‘Upper_Band’] = df_stock[‘SMA_20’] + 2 * df_stock[‘STD_20’]
df_stock[‘Lower_Band’] = df_stock[‘SMA_20’] - 2 * df_stock[‘STD_20’]
# 4. 计算每日收益率和滚动波动率(年化)
df_stock[‘Daily_Return’] = df_stock[‘Close’].pct_change()
df_stock[‘Volatility_30d’] = df_stock[‘Daily_Return’].rolling(window=30).std() * np.sqrt(252) # 年化
# 5. 识别价格突破布林带上轨的事件(潜在超买信号)
df_stock[‘Break_Above_Upper’] = df_stock[‘Close’] > df_stock[‘Upper_Band’]
# 查看结果
print(df_stock[[‘Close’, ‘SMA_10’, ‘SMA_30’, ‘Upper_Band’, ‘Lower_Band’, ‘Volatility_30d’, ‘Break_Above_Upper’]].tail())
# 6. 简单可视化
import matplotlib.pyplot as plt
plt.figure(figsize=(14, 8))
plt.plot(df_stock.index, df_stock[‘Close’], label=‘Close Price’, alpha=0.7, linewidth=1)
plt.plot(df_stock.index, df_stock[‘SMA_10’], label=‘SMA 10’, alpha=0.8)
plt.plot(df_stock.index, df_stock[‘SMA_30’], label=‘SMA 30’, alpha=0.8)
plt.plot(df_stock.index, df_stock[‘Upper_Band’], label=‘Upper Bollinger Band’, linestyle=‘--’, alpha=0.6, color=‘gray’)
plt.plot(df_stock.index, df_stock[‘Lower_Band’], label=‘Lower Bollinger Band’, linestyle=‘--’, alpha=0.6, color=‘gray’)
plt.fill_between(df_stock.index, df_stock[‘Lower_Band’], df_stock[‘Upper_Band’], color=‘gray’, alpha=0.1)
plt.scatter(df_stock.index[df_stock[‘Break_Above_Upper’]], df_stock[‘Close’][df_stock[‘Break_Above_Upper’]], color=‘red’, s=20, label=‘Break Above Upper’, zorder=5)
plt.legend()
plt.title(‘Stock Price Analysis with Rolling Statistics’)
plt.show()
通过这个案例,你可以看到,仅仅使用 rolling() 函数及其衍生出的几个滚动统计量,我们就从原始的价格序列中,构建出了趋势线(SMA)、波动通道(布林带)、风险指标(波动率)和交易信号(突破事件)。这就是滑动窗口分析在实战中的强大之处——它将静态的数据点,转化为了动态的、富含信息的特征序列。
最后再分享一个小技巧:当你对一组数据应用了复杂的 rolling() 计算链后,如果后续需要多次使用这个结果,记得用 .copy() 将其保存到一个新变量,或者用 .assign() 直接赋值给原 DataFrame 的新列。避免重复计算,尤其是在数据量大的时候,这是一个很好的习惯。 rolling() 是一个惰性操作,每次调用 .mean() 、 .std() 都会重新计算一次。
更多推荐




所有评论(0)