尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Pandas滚动与扩展窗口函数:rolling与expanding实战详解

Pandas滚动与扩展窗口函数:rolling与expanding实战详解 1. 从“滑动”到“累积”窗口函数的核心价值如果你用过Excel的数据透视表或者SQL里的OVER (PARTITION BY ... ORDER BY ...)子句那你对“窗口”这个概念应该不陌生。在数据分析的日常里我们经常需要回答一些“动态”的问题比如过去7天的平均销售额是多少从年初到现在的累计利润是多少某个指标最近3个值的趋势是上升还是下降这些问题都有一个共同点它们不是针对整张表的静态计算而是针对数据中一个“移动的片段”或“不断扩大的范围”进行计算。这个“片段”或“范围”就是窗口。Pandas作为Python数据分析的“瑞士军刀”提供了两个强大且直观的工具来处理这类需求rolling和expanding。很多刚接触的朋友可能会觉得它们只是用来算移动平均的这可就太小看它们了。rolling负责的是“滑动窗口”——一个固定大小的窗口沿着数据索引或时间轴一格一格地移动而expanding负责的是“扩展窗口”——窗口从起始点开始随着数据推进窗口大小不断增长直到包含所有历史数据。理解并熟练运用这两个函数能让你从“描述现状”的数据处理者升级为“洞察趋势”的分析师。举个最生活化的例子你看股票K线图那条缠绕在价格线上下波动的“均线”比如5日均线、20日均线本质上就是rolling的杰作。而“今年累计收益率”这种指标则是expanding的典型应用。在业务场景里rolling可以用来监控最近N天的用户活跃度、服务器错误率及时发现异常波动expanding则常用于计算累计注册用户数、年度累计营收等指标。它们让动态分析变得像写一行df[‘col’].mean()一样简单。2.rolling你的“移动分析”显微镜rolling函数的核心是创建一个“滚动窗口”对象。这个对象本身并不立即计算它更像是一个蓝图定义了窗口的规则。之后你可以对这个蓝图施加各种聚合操作如求和、平均、最大值、标准差甚至自定义函数从而在每一个窗口位置上得到计算结果。2.1 基础参数拆解window、min_periods与center创建一个滚动窗口最基本的调用是df[‘column’].rolling(window)。这里的window参数决定了窗口的大小。window参数窗口的“宽度”这个参数可以是整数表示窗口包含的行数也可以是偏移量字符串如‘3D’这在处理时间序列数据时极其有用表示一个3天的时间窗口。注意当使用整数window时它默认是按行数计算的要求索引必须是单调的递增或递减但不一定是等间隔的。如果是时间序列强烈建议使用时间类型的索引DatetimeIndex并配合偏移量字符串这样计算逻辑更清晰能自动处理缺失日期。import pandas as pd import numpy as np # 创建一个简单的日销售数据序列 dates pd.date_range(2023-01-01, periods10, freqD) sales pd.Series([100, 120, 80, 110, 150, 90, 130, 140, 70, 160], indexdates) # 计算3日简单移动平均按行数 sma_by_row sales.rolling(window3).mean() print(3日移动平均按行数:) print(sma_by_row) # 计算3日简单移动平均按时间窗口效果同上例因为这里是连续日 # 如果是非连续日期按时间窗口会更准确 sma_by_time sales.rolling(window3D).mean() print(\n3日移动平均按时间‘3D’:) print(sma_by_time)运行上面代码你会发现前两个结果是NaN。这是因为对于一个窗口大小为3的滚动计算第一个窗口索引0只有1个数据点第二个窗口索引1只有2个数据点都不足以进行“3个数的平均”。这就是min_periods参数要解决的问题。min_periods参数计算的最小数据量要求min_periods默认为window的值。这意味着在窗口大小未满足window之前计算结果都是NaN。你可以通过设置min_periods1来改变这个行为只要窗口内有至少1个数据就进行计算。这在分析初期数据不足时非常有用可以避免开头出现一大片NaN。# 设置min_periods1即使窗口未满也计算 sma_min_periods_1 sales.rolling(window3, min_periods1).mean() print(3日移动平均min_periods1:) print(sma_min_periods_1)现在第一个值就是100它自身的平均第二个值是(100120)/2110从第三个值开始才是标准的3日移动平均。center参数窗口的对齐方式默认情况下窗口是“向右对齐”的。也就是说rolling计算出的值会放在窗口的最后一个位置即当前行。例如在第5行计算出的3日移动平均使用的是第3、4、5行的数据。这符合我们“用过去的数据预测/描述现在”的直觉。 但有些场景下你可能希望结果是对称的或者与当前点居中对齐。这时可以设置centerTrue。此时在第5行计算出的3日移动平均使用的将是第4、5、6行的数据。这在一些信号处理或需要将平滑后的数据与原始数据居中对齐的场景下有用。# 默认向右对齐 sma_right sales.rolling(window3).mean() # 居中对齐 sma_center sales.rolling(window3, centerTrue).mean() print(向右对齐默认:) print(sma_right) print(\n居中对齐:) print(sma_center)注意居中对齐会在序列的开头和结尾产生NaN因为窗口需要向两侧扩展。2.2 不只是平均值丰富的聚合与自定义函数rolling对象支持几乎所有常见的聚合方法mean(),sum(),std()标准差,var()方差,min(),max(),median()中位数,quantile()分位数等。你可以链式调用。# 同时计算移动总和与移动标准差 rolling_obj sales.rolling(window3) result_df pd.DataFrame({ ‘Sales‘: sales, ‘Rolling_Sum‘: rolling_obj.sum(), ‘Rolling_Std‘: rolling_obj.std() # 标准差反映波动性 }) print(result_df)更强大的是你可以使用apply()方法传入自定义函数实现复杂的窗口内计算。比如计算窗口内的极差最大值-最小值或者一个简单的线性回归斜率。# 自定义函数计算窗口内极差 def range_calc(x): return x.max() - x.min() sales.rolling(window3).apply(range_calc)注意apply函数传入的是一个窗口数据的Series你的自定义函数需要能处理这个Series并返回一个标量值。对于性能敏感的大数据操作尽量使用内置聚合方法它们底层是优化过的C代码。自定义apply会慢很多。2.3 时间序列的利器基于时间的滚动窗口当你的索引是DatetimeIndex时rolling的真正威力才显现出来。你可以使用像‘2D’2天、‘5H’5小时、‘1W-MON’每周一为结束的1周这样的偏移量字符串作为window参数。Pandas会基于时间索引智能地确定每个窗口包含哪些数据点即使数据点不是等间隔的。# 创建非等间隔的时间序列数据 irregular_dates pd.to_datetime([‘2023-01-01‘, ‘2023-01-02‘, ‘2023-01-04‘, ‘2023-01-08‘, ‘2023-01-10‘]) irregular_sales pd.Series([10, 20, 30, 40, 50], indexirregular_dates) # 计算一个3天时间窗口的移动和 # 对于‘2023-01-08’这个点窗口是[‘2023-01-06‘, ‘2023-01-08‘]只包含‘2023-01-08‘自身因为01-06没有数据 # 对于‘2023-01-10’这个点窗口是[‘2023-01-07‘, ‘2023-01-10‘]包含‘2023-01-08‘和‘2023-01-10‘ time_rolled_sum irregular_sales.rolling(‘3D‘).sum() print(基于‘3D‘时间窗口的移动和:) print(time_rolled_sum)这个特性对于处理现实世界中不规整的时序数据如日志、交易记录至关重要它能确保计算是基于实际经过的时间而不是单纯的行数。3.expanding追踪“从头至今”的累积故事如果说rolling是看近期趋势的“定焦镜头”那么expanding就是看全局累积的“广角镜头”。它没有固定的窗口大小窗口从第一行开始随着每一行数据的加入而不断扩大直到包含所有已处理的数据。3.1 基本概念与参数创建一个扩展窗口非常简单df[‘column’].expanding(min_periods1)。min_periods参数的含义与rolling中类似表示开始计算所需的最小观测值数量默认为1。这意味着从第一个数据点开始就会进行计算例如第一个点的扩展平均值就是它自己。# 计算累计销售额 cumulative_sales sales.expanding().sum() print(累计销售额:) print(cumulative_sales) # 计算截至每一天的平均销售额 expanding_avg sales.expanding().mean() print(\n扩展窗口平均值历史平均:) print(expanding_avg)扩展窗口计算非常适合生成累积指标累计用户数、年度累计营收YTD、历史平均成本等。它回答了“从开始到现在整体情况如何”的问题。3.2expanding对比cumsum、cumprod你可能会想累计求和用cumsum()不是更简单吗确实对于简单的求和、求积Pandas有专门的累积函数cumsum,cumprod,cummax,cummin。expanding().sum()在结果上等同于cumsum()。那么expanding的价值在哪里统一的接口expanding()返回一个类似rolling的对象你可以用同一套聚合方法mean,std,apply等。如果你想计算历史标准差、历史中位数用expanding().std()、expanding().median()比自己去实现累积逻辑要方便得多。min_periods控制expanding允许你通过min_periods控制从第几个数据点开始计算。比如设置min_periods5可以避免在数据量太少时计算出的统计量如标准差不具代表性。为更复杂的模型做准备expanding窗口是许多时间序列预测模型如滚动预测的基础结构。先熟悉这个接口对后续进阶学习有帮助。# 计算历史标准差要求至少有3个数据点才开始计算 expanding_std sales.expanding(min_periods3).std() print(历史标准差min_periods3:) print(expanding_std)4. 实战进阶rolling与expanding的综合应用与性能陷阱掌握了基础我们来看看如何把它们用在更实际的场景并避开一些常见的坑。4.1 场景一计算带“冷启动期”的移动平均线在量化策略中我们可能不想在数据初期使用太短的均线因为波动太大。我们可以定义一个函数随着时间推移逐渐增加移动平均的窗口。def adaptive_moving_average(series, start_window5, max_window20): 自适应移动平均从start_window开始每过5个数据点窗口增加1直到max_window。 results [] for i in range(len(series)): current_window min(start_window (i // 5), max_window) if i 1 current_window: # 数据不足以填充当前窗口 results.append(np.nan) else: window_data series.iloc[max(0, i1-current_window):i1] results.append(window_data.mean()) return pd.Series(results, indexseries.index) # 应用自适应移动平均 adaptive_ma adaptive_moving_average(sales, start_window2, max_window5) print(自适应移动平均:) print(adaptive_ma)这个例子展示了如何将rolling的逻辑与自定义规则结合。当然对于更复杂的自适应逻辑直接在rolling的apply里写可能会更简洁但需要注意性能。4.2 场景二在DataFrame的多列上应用窗口函数窗口函数可以轻松应用到整个DataFrame的每一列或者通过df.rolling(...)[[‘col1‘, ‘col2‘]]应用到指定的列。# 创建一个包含多指标的DataFrame df pd.DataFrame({ ‘date‘: pd.date_range(‘2023-01-01‘, periods10), ‘revenue‘: [100, 150, 200, 120, 180, 220, 190, 210, 230, 250], ‘cost‘: [60, 80, 110, 70, 90, 130, 100, 115, 125, 140] }).set_index(‘date‘) # 计算3日移动平均应用于所有数值列 df_rolling df.rolling(‘3D‘).mean() print(所有列的3日移动平均:) print(df_rolling) # 计算滚动毛利率(revenue-cost)/revenue # 注意需要先计算滚动的revenue和cost的和再求比率不能直接对毛利率列做rolling mean df[‘rolling_revenue_sum‘] df[‘revenue‘].rolling(3).sum() df[‘rolling_cost_sum‘] df[‘cost‘].rolling(3).sum() df[‘rolling_gross_margin‘] (df[‘rolling_revenue_sum‘] - df[‘rolling_cost_sum‘]) / df[‘rolling_revenue_sum‘] print(\n包含滚动毛利率的DataFrame:) print(df[[‘revenue‘, ‘cost‘, ‘rolling_gross_margin‘]])这里的关键点在于对于由多列计算出的衍生指标如比率、差值通常需要先对原始列进行窗口计算然后再进行衍生计算。直接对衍生指标列做窗口平均得到的结果在数学意义上是不同的。4.3 性能陷阱与优化建议避免在循环中调用rolling/expanding这是最常见的性能杀手。Pandas的窗口操作是向量化优化的一次处理整个Series/DataFrame。如果你在for循环中对每一行单独调用速度会慢几个数量级。错误示范:slow_result [] for i in range(len(sales)): if i 2: slow_result.append(np.nan) else: slow_result.append(sales.iloc[i-2:i1].mean()) # 在循环中切片计算正确做法:fast_result sales.rolling(3).mean() # 向量化操作瞬间完成谨慎使用apply()如前所述apply()会调用Python函数破坏了向量化优势。如果内置聚合方法sum,mean,std等能满足需求绝对不要用apply。对于复杂的窗口计算可以尝试使用np.vectorize或寻找向量化的数学表达方式。处理缺失值rolling和expanding默认会忽略窗口内的NaN值吗答案是取决于你使用的聚合函数。像mean(),sum()这样的函数在计算时会自动跳过NaN。但这也意味着窗口的有效数据点可能少于window大小。如果你希望NaN导致结果为NaN需要在计算前用fillna方法处理数据或者使用min_periods参数进行控制。窗口类型选择Pandas还提供了ewm()指数加权移动窗口它给近期数据更高的权重对于衰减记忆性的序列如金融价格有时比简单rolling平均更合适。不要局限于rolling一种。5. 从Pandas到SQL窗口函数的思想贯通如果你也熟悉SQL会发现Pandas的rolling/expanding与SQL窗口函数Window Functions在思想上高度一致。在MySQL 8.0或SQL Server中你可以这样实现类似功能SQL (模拟3日移动平均):SELECT date, revenue, AVG(revenue) OVER (ORDER BY date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS rolling_avg FROM sales_table;这里的ROWS BETWEEN 2 PRECEDING AND CURRENT ROW就定义了一个大小为3当前行及前两行的滚动窗口。SQL (模拟累计求和):SELECT date, revenue, SUM(revenue) OVER (ORDER BY date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS cumulative_sum FROM sales_table;这里的ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW定义了一个从开头到当前行的扩展窗口。理解了这个对应关系你就能在Pandas和SQL之间自由切换思维。Pandas的语法更贴近Python数据科学生态链式调用非常流畅而SQL窗口函数在数据库端直接计算对于海量数据无需拉取到客户端各有优势。6. 常见坑点与调试技巧索引错乱导致结果不符预期rolling是基于索引顺序的。如果你的索引不是单调的比如被打乱了那么“前3行”在业务逻辑上可能就不是“前3天”。在执行窗口操作前务必使用df.sort_index()确保索引顺序正确特别是对于时间序列数据。window参数为时间字符串时的坑当使用如‘7D‘作为窗口时Pandas是基于时间差来选取数据的而不是基于行数。这意味着如果你的数据在某个7天区间内有10条记录窗口就会包含这10条。这通常是符合业务逻辑的。但要警惕非业务时间如周末、节假日导致的数据空白对窗口内数据量的影响。结果对齐问题默认的rolling计算是“向后看”的这可能导致在实时预测场景中引入“未来数据”。例如用rolling(30).mean()计算出的“当日均线”实际上包含了当日的数据。如果你要模拟一个严格的、只能使用历史数据的预测模型需要使用.shift(1)将结果向下移动一行。expanding也存在同样的问题。大数据下的内存与速度对于超长序列计算一个很大的rolling窗口比如window252用于年线可能会比较慢。如果只需要少数几个窗口统计量可以考虑使用专门优化的库如bottleneckPandas在某些内部计算中已使用它。另外rolling操作会创建一个新的Series/DataFrame如果原数据很大注意内存消耗。我个人在长时间使用中的体会是rolling和expanding最宝贵的价值在于其“声明式”的语法。你只需要告诉Pandas“我想要一个多大或如何扩展的窗口”以及“我想在这个窗口上做什么计算”它就能高效、准确地为你完成。这种抽象让你能更专注于业务逻辑本身而不是陷入实现滑动窗口算法的细节里。下次当你需要对数据做任何“动态”或“累积”的分析时先想想这两个函数它们很可能就是最优雅的解决方案。
返回列表