ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Pandas shift函数详解:时间序列分析与特征工程核心工具

2026/8/4 4:38:43 拓冰建站 浏览量
Pandas shift函数详解:时间序列分析与特征工程核心工具 1. 项目概述为什么我们需要shift在数据分析的日常里我们经常需要处理时间序列或者任何有序的数据。比如你想知道今天的销售额相比昨天是涨了还是跌了你想计算一只股票连续两天的价格差或者你想看看用户连续两次登录的时间间隔这些问题都有一个共同点需要将当前行的数据与它“前面”或“后面”的某一行数据进行比较。手动去索引df.iloc[i]和df.iloc[i-1]当然可以但代码会变得冗长且容易出错尤其是当数据量巨大或者需要做复杂窗口计算时。这时pandas.DataFrame.shift()函数就派上用场了。你可以把它想象成数据框里的一把“尺子”能够精准地将整列数据向上或向下“滑动”指定的位置从而轻松创造出“滞后”或“超前”的序列为差分、百分比变化、移动平均等计算铺平道路。对于任何涉及时间序列分析、特征工程比如创建滞后特征或者简单数据对比的场景shift都是一个不可或缺的利器。2.shift函数核心参数详解shift函数的核心在于其参数的灵活组合理解每个参数是精准操控数据位移的关键。2.1 核心参数periods、freq、axis与fill_valueDataFrame.shift(periods1, freqNone, axis0, fill_valueNone)periods(位移周期数)这是最核心的参数决定了数据移动的方向和步数。正数数据向下移动。例如periods1意味着每一行都取其上一行的值。这常用于创建滞后项。比如今天的温度滞后一天就成了昨天的温度。负数数据向上移动。例如periods-1意味着每一行都取其下一行的值。这可以用于创建超前项或计算反向差分。freq(频率)这是一个非常强大且常用于时间序列的参数。当你的 DataFrame 索引是日期时间类型DatetimeIndex时你可以使用freq参数进行基于时间的位移而不是基于行位置的简单移动。作用它按照指定的时间偏移量如 ‘D’ 代表天’M’ 代表月末’H’ 代表小时来移动索引并保持数据与原始索引对齐。数据本身不会在行与行之间移动而是索引时间发生了变化。示例对于一个日频数据shift(freq’D’)会将所有数据的日期标签向后推一天但数据内容不变。这常用于将未来事件对齐到当前进行分析或者进行时间上的对齐操作。freq和periods是互斥的你只能使用其中一个。axis(轴)控制移动的方向是沿着行0或’index’还是列1或’columns’。axis0 (默认)在垂直方向行上移动数据。这是我们最常用的模式。axis1在水平方向列上移动数据。这个用法相对少见但有时在需要比较相邻列比如比较本月和上月的销售额列时非常有用。fill_value(填充值)数据移动后在序列的头部或尾部会留下空位NaN。fill_value参数允许你指定一个值来填充这些空位而不是默认的NaN。典型用法fill_value0或fill_valuemethod如向前填充。这在某些计算中很有用比如你希望缺失的滞后值用0代替以避免在后续计算中因NaN传播导致整个结果失效。注意fill_value填充的是因移动而产生的绝对空位它不会对移动前就存在的原始NaN值进行填充。理解这一点很重要否则容易对数据清洗产生误解。2.2 参数组合的实战意义理解单个参数后我们来看看组合使用的场景shift(periods2)创建两期滞后。用于分析当前值与两天前/两月前值的关系。shift(freq’2D’)将整个时间索引向后推移两天。假设你有一个活动开始日期列shift(freq’2D’)可以生成一个“两天后”的虚拟日期列用于规划或预警。shift(periods-1, axis1)将每一行的数据向左移动一列。这可以用来快速计算行内相邻列的差值例如df[‘本月’] - df.shift(periods-1, axis1)[‘本月’]可能得到的就是“本月”列减去其右侧的“下月预估”列如果列顺序如此排列。3. 核心应用场景与代码实操理论说再多不如动手试。下面我们通过几个具体的、接地气的例子来看看shift如何解决实际问题。3.1 场景一计算差值与变化率时间序列分析基础这是shift最经典的应用。假设我们有一份每日的股票收盘价数据。import pandas as pd import numpy as np # 创建示例数据 dates pd.date_range(‘2023-10-01’, periods5, freq’D’) df_stock pd.DataFrame({ ‘close’: [100, 102, 101, 105, 108] }, indexdates) print(“原始价格数据”) print(df_stock)目标1计算每日价格变化差分价格变化 今日收盘价 - 昨日收盘价。这里的“昨日收盘价”就是今日收盘价的滞后一期。df_stock[‘price_change’] df_stock[‘close’] - df_stock[‘close’].shift(1) print(“\n计算每日价格变化”) print(df_stock[[‘close’, ‘price_change’]])输出中第一行的price_change会是NaN因为 2023-10-01 没有“前一天”的数据。这正是我们预期的。目标2计算每日价格涨跌幅百分比变化涨跌幅 (今日收盘价 / 昨日收盘价 - 1) * 100%。pandas其实提供了更便捷的pct_change()方法其内部原理就是基于shift实现的。# 使用 shift 手动计算 df_stock[‘pct_change_manual’] (df_stock[‘close’] / df_stock[‘close’].shift(1) - 1) * 100 # 使用内置方法 df_stock[‘pct_change_auto’] df_stock[‘close’].pct_change() * 100 print(“\n计算每日涨跌幅手动 vs 自动”) print(df_stock[[‘close’, ‘pct_change_manual’, ‘pct_change_auto’]])实操心得计算差分时shift(1)得到的是滞后值用当前值减滞后值得到的是正向变化今天减昨天。如果你想计算“反向差分”昨天减今天可以使用shift(-1)。对于金融时间序列pct_change()是更标准且高效的选择它还能处理复利计算等复杂场景。3.2 场景二创建滞后/超前特征特征工程利器在机器学习中我们常常需要利用历史数据预测未来。例如用过去3天的销售额来预测明天的销售额。这就需要创建滞后特征。# 创建一周的销售额数据 df_sales pd.DataFrame({ ‘date’: pd.date_range(‘2023-10-09’, periods7, freq’D’), ‘sales’: [200, 220, 210, 250, 230, 260, 240] }) df_sales.set_index(‘date’, inplaceTrue) # 创建滞后1期、2期、3期特征 for lag in [1, 2, 3]: df_sales[f’sales_lag_{lag}’] df_sales[‘sales’].shift(lag) print(“创建滞后特征后的数据”) print(df_sales)现在对于 2023-10-12 这一行我们不仅有当天的销售额250还有前三天lag_1: 210,lag_2: 220,lag_3: 200的销售额。这些lag_x列就可以作为特征输入到预测模型中。目标创建超前特征用于回填有时我们可能需要用“未来”的数据来修正或解释当前数据。例如在事件分析中知道后续结果后回看初始指标。# 创建超前一期特征例如明天的销售额 df_sales[‘sales_lead_1’] df_sales[‘sales’].shift(-1) print(“\n创建超前特征Lead后的数据”) print(df_sales[[‘sales’, ‘sales_lead_1’]])注意最后一行的sales_lead_1是NaN因为后面没有数据了。3.3 场景三计算移动统计量如移动平均虽然pandas有专门的.rolling()方法计算移动窗口统计但理解其与shift的关系有助于更灵活地定制计算。一个简单的等权重双日移动平均可以这样实现# 简单双日移动平均 (今天 昨天) / 2 df_stock[‘close_ma_2’] (df_stock[‘close’] df_stock[‘close’].shift(1)) / 2 print(“\n计算双日简单移动平均”) print(df_stock[[‘close’, ‘close_ma_2’]])更复杂的窗口计算如加权移动平均、指数移动平均通常直接使用.rolling().mean()、.ewm().mean()但shift可以用于对计算结果进行对齐或进一步处理。3.4 场景四freq参数的独特应用时间索引位移当你的索引是时间时freq参数提供了另一种维度的“位移”。它移动的是时间标签而不是数据在表格中的位置。# 创建一个带有时间索引的DataFrame df_time pd.DataFrame({ ‘value’: [10, 20, 30, 40] }, indexpd.date_range(‘2023-01-01’, periods4, freq’D’)) print(“原始数据按时间索引”) print(df_time) # 使用 periods 移动数据 df_time[‘value_shift_period’] df_time[‘value’].shift(1) print(“\n使用 periods1 移动数据”) print(df_time[[‘value’, ‘value_shift_period’]]) # 使用 freq 移动时间索引 df_time[‘value_shift_freq’] df_time[‘value’].shift(freq’D’) print(“\n使用 freq’D’ 移动时间索引”) print(df_time[[‘value’, ‘value_shift_freq’]])仔细观察输出value_shift_period数据向下移动了一行。2023-01-02 的值 20 被移到了 2023-01-03 的位置上索引没变数据位置变了。value_shift_freq数据的时间索引向后移动了一天。2023-01-01 的值 10 现在被标记为 2023-01-02数据本身没动但它的“时间戳”变了。这在需要将未来事件对齐到当前时间点进行分析时非常有用。4. 高级技巧与性能考量掌握了基本用法后我们来看看如何更高效、更安全地使用shift。4.1 处理NaN值的策略数据移动必然产生NaN。如何处理它们至关重要。保留NaN在后续计算如.diff(),.pct_change()或模型训练中许多库如sklearn无法处理NaN需要先处理。填充固定值shift(fill_value0)。适用于缺失值有明确业务含义如“无交易视为0”的场景。前向/后向填充通常不直接在shift里做而是在shift之后用.fillna(method’ffill’)或.bfill()。例如创建滞后特征后你可能想用最近的有效值填充最开始的几个NaN。df[‘lag_1_filled’] df[‘value’].shift(1).fillna(method’ffill’)注意填充会改变数据的统计性质在时间序列预测中需谨慎因为这相当于“用未来信息填充了过去”可能导致前瞻性偏差Look-ahead Bias。4.2 与groupby联用组内位移这是shift一个极其强大的功能。当你需要计算每个分组如每个用户、每只股票内部的时间序列变化时必须结合groupby否则不同组的数据会混在一起。# 示例多个用户每天的登录次数 df_log pd.DataFrame({ ‘user_id’: [‘A’, ‘A’, ‘A’, ‘B’, ‘B’, ‘B’], ‘date’: pd.date_range(‘2023-10-01’, periods3, freq’D’).tolist() * 2, ‘login_count’: [5, 3, 8, 2, 4, 1] }) df_log.sort_values([‘user_id’, ‘date’], inplaceTrue) # 确保按用户和时间排序 # 错误做法直接对整个DataFrame使用shift df_log[‘wrong_lag’] df_log[‘login_count’].shift(1) print(“错误做法跨用户混合位移”) print(df_log) # 正确做法按用户分组后shift df_log[‘correct_lag’] df_log.groupby(‘user_id’)[‘login_count’].shift(1) print(“\n正确做法组内位移”) print(df_log)可以看到错误做法中用户B的第一天数据错误地滞后成了用户A的最后一天数据。而正确做法下每个用户组内独立计算滞后用户B第一天的滞后值为NaN。4.3 性能优化建议避免在循环中调用shift如果你需要创建多个滞后特征像之前例子那样使用for循环在列表推导中完成是可行的。但对于超大数据集可以考虑使用numpy的roll函数进行底层操作或者利用pandas的assign方法配合字典推导式。# 相对高效的多滞后特征创建 lags [1, 2, 3, 7] lag_data {f’lag_{l}‘: df[‘value’].shift(l) for l in lags} df df.assign(**lag_data)注意数据类型shift操作会保持原数据类型。但如果fill_value的类型与列类型不兼容可能会导致整列数据类型被向上转换如int变成float增加内存消耗。索引排序对于时间序列数据在执行shift或groupby().shift()前务必确保数据按照时间索引或排序列正确排序否则位移结果将毫无意义。5. 常见陷阱、疑难排查与实战心得即使明白了原理在实际使用中还是会踩坑。下面分享一些我踩过的“坑”和解决方法。5.1 陷阱一索引错乱与数据对齐pandas的运算基础是索引对齐。shift移动数据后索引保持不变。这有时会导致意想不到的结果。df pd.DataFrame({‘A’: [1, 2, 3]}, index[‘x’, ‘y’, ‘z’]) df[‘A_shifted’] df[‘A’].shift(1) print(df) # 输出 # A A_shifted # x 1 NaN # y 2 1.0 # z 3 2.0值1从索引 ‘x’ 移动到了索引 ‘y’ 的位置。如果你后续想按索引合并或比较必须清楚这一点。shift不改变索引只改变数据相对于索引的位置。5.2 陷阱二fill_value的误解fill_value只填充因移动产生的缺口不填充原始NaN。s pd.Series([1, np.nan, 3, 4]) print(“原始序列:”, s.values) print(“shift(1)后:”, s.shift(1).values) print(“shift(1, fill_value0)后:”, s.shift(1, fill_value0).values)输出原始序列: [ 1. nan 3. 4.] shift(1)后: [nan 1. nan 3.] shift(1, fill_value0)后: [ 0. 1. nan 3.]看第二个元素原始的nan在两种情况下都仍然是nan。fill_value0只填充了第一个因移动产生的新nan。5.3 疑难排查结果全是NaN或不符合预期检查数据类型确保你操作的列是数值型或至少是可以进行位移操作的类型。对象类型的列可能因为元素不可移位而导致错误。检查索引是否是无序的是否在groupby后忘记排序无序索引下的shift结果不可预测。检查periods正负号这是最常见的错误之一。想要滞后用过去的数据却写了负号结果变成了超前。确认axis如果你是在处理多列数据并期望按行移动却错误设置了axis1结果会截然不同。freq与periods混淆记住用了freq就不能用periods。如果你指定了freq但索引不是DatetimeIndex、TimedeltaIndex或PeriodIndex将会报错。5.4 实战心得记录命名规范创建滞后特征时列名最好包含_lag_或_lead_以及期数如sales_lag_7这样一目了然便于后续特征选择和模型解释。数据边界处理在构建预测模型时如何处理最初几行因滞后产生的NaN通常有两种策略(1) 直接删除这些行df.dropna(inplaceTrue)适用于数据量大的情况(2) 用特定方法填充如均值、中位数但需在交叉验证中小心处理避免数据泄露。我个人的经验是对于严格的时序预测倾向于删除以保持数据的纯净性。shift是视图还是拷贝shift操作返回的是一个新的Series或DataFrame而不是原数据的视图。这意味着修改shift后的结果不会影响原始数据。放心使用。与diff()和pct_change()的关系df[‘col’].diff(1)本质上等价于df[‘col’] - df[‘col’].shift(1)。pct_change()也是基于shift的。了解这一点你就能在需要自定义变化率计算比如使用对数收益率np.log(df[‘col’] / df[‘col’].shift(1))时游刃有余。shift函数就像一把瑞士军刀简单但功能多样。从最基础的差分计算到复杂的特征工程它都是数据处理流水线上不可或缺的一环。理解其核心参数、掌握其应用场景、并避开常见的陷阱能让你在数据分析和建模工作中更加得心应手。刚开始使用时建议在小数据集上多试几次观察输入和输出的对应关系很快就能形成直觉。记住在时间序列的世界里让数据“动起来”进行比较往往是发现洞见的第一步。