ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Pandas时间序列处理实战:从类型转换到重采样与滚动窗口特征工程

Pandas时间序列处理实战:从类型转换到重采样与滚动窗口特征工程 1. 先把数据喂给Pandas类型转换与索引才是地基很多人一上来就直接调resample()或rolling()结果报错一堆或者画出来的图横轴乱七八糟。这里百分之八十的问题都出在同一个根子上你的时间列根本不是Pandas认识的时间类型。Pandas里处理时间序列第一步永远是把纯文本或数字形式的时间列转成datetime64类型然后把它设为索引。这两步不做扎实后面全都是空中楼阁。先看最常见的读取方式。假设你手里有一份CSV日期列长这样2024-05-01、2024/05/01、20240501甚至还有带时分秒的。我用pd.read_csv()读完先不急着操作第一件事是检查dtypesimport pandas as pd df pd.read_csv(sales_data.csv) print(df.dtypes)如果看到object或者int64那就说明时间列还没被识别。这时用pd.to_datetime()做转换它是整个时间序列处理里最核心的入口函数df[date] pd.to_datetime(df[date], format%Y-%m-%d)这里我强烈建议不要省略format参数。pd.to_datetime()虽然自带推断能力但遇到2024/05/01这种斜杠格式或者01-05-2024这种歧义格式时自动推断有可能猜错尤其是在月/日和日/月之间摇摆的场景。主动传format相当于告诉解析器别猜了按这个规则来速度更快也更稳。如果数据量上了几十万行这个参数带来的性能差异非常明显。类型转完之后把时间列设为索引df df.set_index(date).sort_index()sort_index()同样容易被忽略。时间序列分析有个默认前提索引是升序的。你的原始数据如果按门店、按产品分组排列过时间索引必然是乱的不排序会导致后面的重采样、窗口计算出现莫名其妙的错位而且定位问题时很难察觉。还有一种常见情况是从Excel或数据库里读出来的时间列里面混着NaT或空字符串。pd.to_datetime()遇到无法解析的值会直接抛错这时候加上errorscoerce可以把非法值统一转成NaT后面再统一处理缺失。但注意这个参数会让你吃掉所有异常值最好先打印一下转换前后的行数差确认究竟丢了多少数据。有个小技巧我觉得很实用如果你只想提取年、月、日、星期这些字段没必要生成一列新数据直接用.dt访问器就能取df[year] df.index.year df[month] df.index.month df[weekday] df.index.weekday # 0代表周一要是还想判断某天是不是月末Pandas的.is_month_end可以直接返回布尔值做财务月度结算对账时特别省事。所以真正的时间序列处理九成的工作量在数据进pandas之前就已经开始了。把数据清洗和时间序列工具分开看是很多新手走弯路的重要原因。我习惯的做法是拿到数据先建一个单独的清洗脚本把类型转换、索引设置、缺失值处理全部做完输出一个干净的中间文件再进入分析环节。这样一来后面做任何重采样、滚动窗口、特征工程面对的都是已经标准化了的数据心理负担小很多。2. 重采样把零散记录按时间格子重新码放重采样是时间序列里最有实用价值、也最容易被误用的一类操作。它的本质很简单把不规则的时间点映射到固定的时间格子里然后对每个格子的数据做聚合。你可以把它理解成整理货架——散落一地的商品原始数据点按保质期月份时间格子重新归位然后数一数每个格子有多少件聚合函数。Pandas的重采样函数是resample()它需要配合一个频率字符串来指定时间格子的大小。常用的频率有这些频率代码含义典型场景D自然日按天统计销量、流量W自然周默认周日结束周报统计M自然月月末月度财务结算Q自然季度季度复盘H/30min小时/半小时监控大盘、日志聚合W-MON从周一开始的周业务周周一为起点MS月初每月第一天对比比如说你有一份每笔订单的明细数据想看每天的总销售额和订单数做法是daily df.resample(D).agg({ amount: sum, order_id: count })这段代码做的事情非常直白把每笔订单按日期归类然后对amount求和、对order_id计数。agg()里可以传字典对不同列做不同的聚合操作这比先求和再合并要简洁得多。更妙的是你还能在同一个调用里同时算均值、最大值、最小值daily df.resample(D).agg({ amount: [sum, mean, max, min] })这样返回的是一个带多层列索引的DataFrame列名变成(amount, sum)这种形式。看的时候可能需要df.columns调整一下或者用pd.NA占位。实际项目里我更喜欢先resample再分别计算逻辑更清晰排查问题也方便。重采样有个关键的坑结果索引的宽度由原始数据的时间范围决定而不是由你期望的格子数量决定。如果原始数据只有一周你resample(M)也就只能得到一个月的格子如果原始数据横跨三年即使中间有连续几个月的空窗期Pandas也会忠实地为每个月份生成一行NaN。这个特性在做节假日较多的行业数据时很烦人但反过来也是优点——它能让你一眼看出数据缺失的情况。说到缺失resample()之后经常会遇到某些格子没有任何原始数据的情况默认生成NaN。这时可以用.ffill()或者.bfill()填空也可以用.fillna(0)直接补零到底用哪种取决于业务含义缺数据等于没有还是等于未知。我的经验是如果做的是销量类指标空缺时间点通常可以视为0如果是传感器读数这类连续型数据用插值或者前向填充更合理。另一个容易踩坑的点是时区。resample()默认按索引的本地时间切格子。如果你的数据带时区偏移比如UTC8并且你想按UTC日切格子最好先转换时区再重采样不要依赖默认行为否则节假日、换日点和夏令时的边界会让你的统计口径混乱。Pandas提供了.tz_convert()方法处理这类问题后面第4节我会详细展开。还有一个进阶用法值得掌握resample()可以搭配apply()做任意自定义聚合。比如你想算每天的价格波动区间可以先按天分组再对最高价减最低价price_range df[price].resample(D).apply(lambda x: x.max() - x.min())这种做法在股票、期货数据分析里几乎是标配。但你得知道apply()的性能比内置聚合函数慢一个数量级如果数据量大建议先用.groupby()按日期分组再配合pd.NamedAgg做并行计算。3. 滚动窗口与shift滞后特征和移动平均的实用套路如果你做过机器学习预测任务一定对特征工程这个词不陌生。时间序列的特征工程核心就是两件事滑动窗口统计和滞后值构造。这两招几乎能覆盖金融时序预测、销量预估、异常检测等一大半场景。先说rolling()滚动窗口。它的思路是固定一个窗口宽度比如7天从序列起点开始每次往后移动一格对窗口内的数据做聚合。rolling(7).mean()就是最常见的7日移动平均用来平滑短期波动、观察趋势。df[sales_7d_avg] df[sales].rolling(window7).mean()这里的window7表示用当前行以及前6行的数据求平均。如果你希望窗口是过去7天而不是前7行数据那就得注意采样的频率一致性。如果数据是按天采样的window7刚好是7天如果数据按小时采样想算7天均值就得写rolling(window168)或者用rolling(7D)。Pandas从0.19版本开始支持基于时间的滚动窗口写rolling(7D)时窗口宽度按时间戳计算即使中间有缺失的行或者数据频率不均匀窗口也能正确对齐。这是一个非常实用的特性尤其是在处理不规则时间戳的数据时能让代码更贴近业务语义。再来看shift()滞后值。它的作用和SQL里的LAG()函数一模一样把某一列的值向上平移若干行让昨天的值出现在今天的行里这样模型就能用昨天的数据预测今天的结果。df[sales_lag1] df[sales].shift(1) df[sales_lag7] df[sales].shift(7)这里的shift(1)是把整列下移一行第0行变成NaNshift(7)则是下移7行。做预测模型时滞后1期和滞后7期周同期是最常用的两个特征。rolling()和shift()组合起来能构建出一大类特征。比如我想算过去7天里有多少天销量超过了当天的销量这种问题听起来复杂其实一行代码就能搞定df[count_gt_today] (df[sales].shift(1).rolling(7).apply( lambda x: (x df[sales].loc[x.index[-1] 1]).sum() ))这段代码稍微有点绕我解释一下shift(1)先把昨天的序列准备好然后.rolling(7)取过去7天再用apply()自定义逻辑——窗口内的每一天跟当前日比较。这里有个隐含的坑rolling()的窗口是左闭右开的x.index[-1]是窗口最后一天对应的今天需要加1偏移。这种写法适合做数据探索但不适合大规模生产环境因为逐行计算太慢。如果你对性能有要求更推荐用pd.Series.rolling().corr()或cov()这类内置方法它们底层用Cython优化速度比apply()快一个数量级。还有一种场景值得单独拿出来说多个时间序列之间的滚动相关性。比如你有两个产品的销量序列想观察它们在过去30天内的相关性变化可以这样写roll_corr df[product_a].rolling(30).corr(df[product_b])这行代码可以说是股票配对交易、电商交叉销售分析里的高频操作。它返回的序列每个点都代表过去30天里两个序列的相关系数能直观反映两个变量关系的动态变化。最后提醒一个新手特别容易犯的错误做完rolling()或shift()之后新生成的特征在序列头部会产生大量NaN因为窗口还没填满。直接把含NaN的特征喂给机器学习模型很多算法会直接报错或者把NaN当成一个值处理。我习惯先df.dropna()丢掉头部这几行或者用bfill()补上具体取决于你的数据量和建模策略。如果数据量够大丢掉最前面的7行根本无所谓但如果样本本身就少就得考虑互补策略了。4. 时间序列的缺失值与时区两个常常被忽略的坑缺失值处理在任何数据分析里都是重头戏但在时间序列里它有一个特殊性时间顺序本身就是信息。你不能简单地把NaN整行删掉因为删除会破坏时间连续性也不能盲目填0因为0可能代表无数据而不是数值为0。不夸张地说缺失值填得对不对直接影响后续重采样、滑窗、预测的质量。我先说最简单的情况时序数据里的NaN是孤立存在的比如某个小时缺了一条记录。处理方式无非三种——删除、填充、插值。删除是最省事的方式但代价是丢掉其他列的有效信息。填充则要分业务场景如果是销量、流量这类累计型指标前向填充ffill()很常用——今天没数据默认沿用昨天的值如果是传感器读数这类连续型指标线性插值interpolate()更合理——两个已知点之间数值大概率是渐变的而不是突变的。# 前向填充用上一个有效值补缺口 df[sales].fillna(methodffill) # 线性插值在缺失点之间拉一条直线 df[temperature].interpolate(methodlinear)interpolate()的默认方法是线性插值对大多数场景够用了。如果你处理的是带周期性的数据比如温度、电量消耗methodtime会让插值基于时间间隔进行计算比默认的等间距插值更符合实际。举个例子假设两个观测点之间实际间隔了12小时而数据里只隔了1行默认插值会把中点当作半行的位置而methodtime会老老实实按12小时的中点来算结果自然更合理。如果说缺失值处理是明坑时区问题就是暗坑。很多数据分析师在项目初期根本意识不到时区的存在直到某一天发现明明按小时聚合的数据跨天的时候总是少一个小时或者多一个小时。这就是时区偏移在捣鬼。Pandas处理时区有两种典型方式。第一种是数据本身就是UTC时间你想展示成本地时间df.index df.index.tz_localize(UTC).tz_convert(Asia/Shanghai)tz_localize()做的动作是给一个没有时区信息的时间戳加上时区标记tz_convert()则是把已标记的时区转换成另一个时区。两者必须搭配使用顺序不能反。如果你的索引已经带了时区信息再做tz_localize()会直接报错。第二种场景是数据本身带偏移比如2024-05-01 08:00:0008:00你想统一成某个基准时间df.index df.index.tz_convert(UTC)这里要注意tz_convert()不会改变时间戳的绝对时刻只是换了个钟表显示而已。2024-05-01 08:00:0008:00转换成UTC后变成2024-05-01 00:00:0000:00两者代表的是同一个瞬间。在实际业务里我见过最典型的时区事故是跨天统计。比如一个电商平台订单时间记录的是UTC8但数据库里存的是UTC。如果直接用UTC做resample(D)每天的边界就是UTC的零点也就是北京时间的早上8点。这时候某天的订单量这个口径就完全错了。正确的做法是先转换时区再做频率聚合df.index df.index.tz_convert(Asia/Shanghai) daily df.resample(D).sum()另外一个时区相关的隐藏操作是dst夏令时处理。虽然我们这边不实行夏令时但如果数据源是海外业务夏令时切换的那一天时间索引会出现重复的一小时或者缺失的一小时。Pandas在tz_localize()时遇到不存在的本地时间会抛错你可以用参数ambiguousNaT把这些时间点置空或者用nonexistentshift_forward把不存在的时刻自动顺延一小时。实际项目中我建议先检查数据里有没有date_range异常df.index.is_monotonic_increasing和df.index[df.index.duplicated()]配合排查能快速定位问题区域。还有一个常被忽略的点时区信息会影响rolling()跨天窗口的计算。同样是rolling(3D)UTC8和UTC的数据窗口起止点完全不同。所以我一般会在数据加载阶段就把时区统一好后续所有操作都在统一时区内进行避免分析到一半发现跨天边界歪了这种糟心事。5. 完整实战从CSV到特征工程的一站式流程前面讲了这么多理论和方法现在把它们串起来走一个完整的实战流程。我用一个电商场景模拟原始订单表有订单时间、产品ID、销售额目标是构建一份按天、按产品聚合的销售特征表方便后续做销量预测或异常检测。5.1 数据加载与清洗import pandas as pd df pd.read_csv(orders.csv, parse_dates[order_time]) df[order_date] df[order_time].dt.normalize() # 去掉时分秒只留日期这里用parse_dates参数让pandas在读取时就完成日期解析省去手动to_datetime()的步骤。dt.normalize()把2024-05-01 14:23:11归一化成2024-05-01 00:00:00方便后续按天聚合。接着处理缺失和异常值df df.dropna(subset[order_time, amount]) df df[df[amount] 0]假设产品ID也有缺失补齐的方式取决于ID的语义。如果是内部编码缺失说明数据质量有问题建议直接剔除如果是外部分类缺失的可以统一标为未知。5.2 聚合出时间序列这一步的目标是把订单明细变成每天每个产品的销售额序列ts df.groupby([order_date, product_id])[amount].sum().reset_index()groupby返回的结果索引还是普通的整数索引所以得.reset_index()把它变回规整的DataFrame。如果想直接得到一个以时间为索引的序列可以不用reset_index()让Pandas保留多层索引后续重采样会更方便ts df.groupby([order_date, product_id])[amount].sum()多层索引的重采样稍微有点绕你得先unstack()再resample()ts_pivot ts.unstack() # 行日期列产品ID ts_daily ts_pivot.resample(D).sum()unstack()把产品ID从行索引搬到列索引此时每列就是一个独立的产品销售序列。resample(D)把索引统一成自然日同一天多笔订单自动累加。这一步做完就得到了一张标准的宽表每一行是一个日期每一列是一个产品的销售额。5.3 构建特征窗口假设我们要做销量预测目标是预测明天每个产品的销售额。那就需要为每个产品构造滞后特征和滚动统计量。因为已经在宽表结构里可以按列循环处理for col in ts_daily.columns: ts_daily[f{col}_lag7] ts_daily[col].shift(7) ts_daily[f{col}_rolling7_mean] ts_daily[col].rolling(7).mean() ts_daily[f{col}_rolling7_std] ts_daily[col].rolling(7).std()这里的lag7用来捕捉上周同期的规律rolling7_mean和rolling7_std用来捕捉最近一周的整体水平与波动程度。对于周维度有明显周期性的业务周内高、周末低这三个特征组合起来信息量相当可观。如果产品非常多成千上万循环写法的性能会很难看。这时更高效的做法是利用Pandas的窗口函数在DataFrame上直接并行操作lag7 ts_daily.shift(7) roll_mean ts_daily.rolling(7).mean() roll_std ts_daily.rolling(7).std() # 给列名加后缀 lag7.columns [f{c}_lag7 for c in lag7.columns] roll_mean.columns [f{c}_roll_mean for c in roll_mean.columns] roll_std.columns [f{c}_roll_std for c in roll_std.columns] feat pd.concat([lag7, roll_mean, roll_std], axis1)pd.concat把三个特征表按列拼起来一次性得到全产品的特征矩阵。这种方式不仅代码简洁而且利用了pandas底层的向量化计算速度和循环不是一个量级。5.4 处理NaN并拆分数据集特征建好之后前7行因为窗口没填满一定全是NaN。我习惯先用dropna()把头部删掉再按时间顺序切分训练集和测试集feat feat.dropna() train feat[feat.index 2024-04-01] test feat[feat.index 2024-04-01]时间序列切分和普通机器学习不同绝对不能随机打乱。你要预测未来就必须用过去的数据训练、未来的数据验证一旦混入未来信息所有评估指标都会虚高模型上线就现原形。这是新手最容易犯的错误。5.5 接入模型到了这一步数据已经是标准的特征矩阵目标列结构了。用LightGBM、XGBoost或者线性回归都可以。我在实际项目里常看到一种粗糙做法——直接把原始序列扔给LSTM却忽略了特征工程。其实对于销量、流量这类强周期业务只要把滞后特征和滚动统计做扎实树模型往往就够用了深度模型未必有优势。如果你坚持要用LSTMPandas这边能做的准备是把数据转成numpy数组按固定时间步长构造样本序列。这一步可以自己写循环切分也可以用keras.preprocessing.timeseries_dataset_from_array。老实说我试过几次之后觉得除非原始序列特别长且复杂否则shiftrolling构造的特征比直接喂原始序列效果更稳定调试成本也低得多。6. 我在实际项目中的一些取舍经验到这里Pandas时间序列的主要操作都过了一遍。最后聊几句我的个人经验可能比前面的代码更值得你留意。第一能用内置方法就别用apply()。Pandas的rolling、resample内置聚合函数大多是Cython编译的速度优势非常明显。我见过有人用apply(lambda x: x.mean())去算移动平均数据量一上10万行跑一次要几分钟换成.mean()瞬间出结果。写代码时多花10秒钟查一下有没有内置方法能省下后面大量的等待时间。第二时间序列处理一定要先画图。数据分析里分箱统计再复杂都不如一张折线图直观。拿到数据先df.plot()看一眼整体形态——有没有断崖、有没有缺失区间、有没有明显的异常尖峰比跑十行统计代码更高效。Pandas底层用matplotlib渲染df.plot()基本是零成本出图ts_daily.plot(figsize(12, 6))如果发现图形断裂或者剧烈抖动大概率是数据清洗环节出了遗漏。第三时区和频率口径在一开始就要定下来。我踩过的坑是项目做到一半发现数据源有UTC和本地时间混用结果所有统计都要推翻重来。现在我的习惯是读取数据的第一时间就统一成目标时区然后给每列备注清楚业务口径——这个值是本地时间还是UTC按天统计边界是零点还是早上8点这些信息写进代码注释里能避免不少返工。第四绝对不要在模型里泄露未来信息。做滞后特征时shift(n)意味着n期之前的数值是已知的。如果你在特征构建过程中不小心用到了rolling(window7).mean()而窗口宽度包含了当期那这个特征本身就包含了今天的值用今天预测明天训练时效果好得惊人测试时立刻崩溃。有一个简单的自查方法对每条样本确认它所有特征的时间戳都严格早于目标时间戳。说了这么多其实Pandas的API用久了会形成肌肉记忆但真正决定你时间序列分析质量的永远是对业务的理解 对数据的敬畏。工具只是把思路变成现实的手段两件套缺一不可。希望这篇内容能帮你在处理时间序列数据的时候少走几步弯路。按照这个实战流程走一遍从原始CSV到最后喂给模型的特征矩阵整个过程大概是读取数据 - 类型转换 - 索引设置 - 重采样 - 滚动窗口 - 滞后特征 - 缺失处理 - 删除头部 - 切分训练集。每一步都是有明确目的的不是机械地堆代码。你可以在自己的数据集上按这个链路跑一遍遇到问题再回到对应的章节排查应该能顺畅不少。
返回列表