【精选优质专栏推荐】


每个专栏均配有案例与图文讲解,循序渐进,适合新手与进阶学习者,欢迎订阅。

在这里插入图片描述

引言

在处理时间序列数据时,经常会遇到一些反复出现的模式:计算移动平均值、检测异常峰值、为预测模型构造特征。大多数分析人员往往会编写冗长的循环和复杂的函数来完成这些操作,但实际上,借助 NumPy,可以用一行优雅且易于维护的代码解决。

NumPy 的数组操作能够简化大多数常见的时间序列任务。与其逐步思考数据的转换过程,不如直接利用矢量化操作一次性处理整个数据集。

本文将介绍 10 个常用于时间序列分析的一行 NumPy 技巧,帮助你更高效地应对常见任务。

示例数据
我们先创建一组逼真的时间序列数据,用于后续的一行代码示例:

import numpy as np
import pandas as pd
from datetime import datetime, timedelta
 
# 创建示例时间序列数据
np.random.seed(42)
dates = pd.date_range('2023-01-01', periods=100, freq='D')
trend = np.linspace(100, 200, 100)
seasonal = 20 * np.sin(2 * np.pi * np.arange(100) / 30)
noise = np.random.normal(0, 5, 100)
values = trend + seasonal + noise
 
# 额外示例数据
stock_prices = np.array([100, 102, 98, 105, 107, 103, 108, 112, 109, 115])
returns = np.array([0.02, -0.03, 0.05, 0.01, -0.02, 0.04, 0.03, -0.01, 0.02, -0.01])
volumes = np.array([1000, 1200, 800, 1500, 1100, 900, 1300, 1400, 1050, 1250])

有了这些样本数据,我们就可以进入一行代码的演示。


1. 构造滞后特征(Lag Features)

滞后特征通过将时间序列向后平移,捕捉时间依赖关系,是自回归类模型必备的输入。

# 构造多个滞后特征
lags = np.column_stack([np.roll(values, i) for i in range(1, 4)])
print(lags)

输出(部分):

[[217.84819466 218.90590418 219.17551225]
 [102.48357077 217.84819466 218.90590418]
 [104.47701332 102.48357077 217.84819466]
 [113.39337757 104.47701332 102.48357077]
 ...
 [217.47142868 205.96252929 207.85185069]
 [219.17551225 217.47142868 205.96252929]
 [218.90590418 219.17551225 217.47142868]]

结果矩阵中,每一列分别代表向后平移 1、2、3 个时间步的序列。前几行出现了从序列末尾环绕回来的值。


2. 计算滚动标准差(Rolling Standard Deviation)

滚动标准差是衡量波动性的常用指标,尤其适合风险评估。

# 5 期滚动标准差
rolling_std = np.array([np.std(values[max(0, i-4):i+1]) for i in range(len(values))])
print(rolling_std)

输出(部分):

[ 0.          0.99672128  4.7434077   7.91211311  7.617056    6.48794287 ...
  6.45696044  6.19946918  5.74848214  4.99557589]

得到的数组显示了随时间推移的波动性变化。序列的前几项由于样本不足,基于较少的点计算。


3. 基于 Z 分数的异常点检测(Outlier Detection)

检测异常点有助于发现市场突发事件或数据质量问题。

# 标记超过 2 个标准差的异常点
outliers = values[np.abs((values - np.mean(values)) / np.std(values)) > 2]
print(outliers)

输出:

[217.47142868 219.17551225 218.90590418 217.84819466]

这会返回一组显著偏离均值的值,可用于标记异常时期。


好的,我来把 第 4 到第 10 个示例翻译成书面化中文,保持和之前相同的格式与代码结构。


4. 计算指数移动平均(Exponential Moving Average, EMA)

相比于普通的移动平均,有时需要指数移动平均,它会对最新的数据赋予更高权重,从而对趋势变化更为敏感。

ema = np.array([values[0]] + [0.3 * values[i] + 0.7 * ema[i-1] for i, ema in enumerate([values[0]] + [0] * (len(values)-1)) if i > 0][:len(values)-1])
print(ema)

上述写法不会按预期运行,因为指数移动平均的计算本质上是递归的,而递归在向量化操作中并不直观。执行此段代码会抛出 TypeError 异常。不过可以自行在 Notebook 中取消注释后尝试。

以下是更清晰且可运行的写法:

# 更易读的 EMA 计算
alpha = 0.3
ema = values.copy()
for i in range(1, len(ema)):
    ema[i] = alpha * values[i] + (1 - alpha) * ema[i-1]
print(ema)

输出(部分):

[102.48357077 103.08160353 106.17513574 111.04294223 113.04981966 ... 
 200.79862052 205.80046297 209.81297775 212.54085568 214.13305737]

结果为一条平滑的曲线,相比简单移动平均能更快响应最新数据的变化。


5. 寻找局部极大值和极小值

峰值和谷值的检测对识别趋势反转以及支撑位或阻力位非常重要。下面寻找局部极大值:

# 寻找局部峰值(极大值)
peaks = np.where((values[1:-1] > values[:-2]) & (values[1:-1] > values[2:]))[0] + 1
print(peaks)

输出:

[ 3  6  9 12 15 17 20 22 25 27 31 34 36 40 45 47 50 55 59 65 67 71 73 75
 82 91 94 97]

结果是一个索引数组,指示了局部极大值的位置,可用于识别潜在的卖点或阻力位。


6. 根据价格变化计算累计收益率

有时将绝对价格变化转化为累计表现指标更有意义。

# 根据每日收益率计算累计收益率
cumulative_returns = np.cumprod(1 + returns) - 1
print(cumulative_returns)

输出:

[ 0.02       -0.0106      0.03887     0.0492587   0.02827353  0.06940447
  0.1014866   0.09047174  0.11228117  0.10115836]

结果表示随时间推移的总收益率,这是绩效分析和投资组合跟踪中的重要指标。


7. 数据归一化到 0-1 区间

最小-最大缩放确保所有特征映射到 [0,1] 区间,避免因特征量纲差异而影响分析。

# Min-max 归一化
normalized = (values - np.min(values)) / (np.max(values) - np.min(values))
print(normalized)

输出(部分):

[0.05095609 0.06716856 0.13968446 0.21294383 0.17497438 0.20317761 ...
 0.98614086 1.         0.9978073  0.98920506]

结果为缩放到 0 和 1 之间的序列,在保留分布形状的同时实现标准化。


8. 计算百分比变化

百分比变化提供了与尺度无关的波动度量。

# 计算相邻时间点之间的百分比变化
pct_change = np.diff(stock_prices) / stock_prices[:-1] * 100
print(pct_change)

输出:

[ 2.         -3.92156863  7.14285714  1.9047619  -3.73831776  4.85436893
  3.7037037  -2.67857143  5.50458716]

结果为一个数组,显示各时期之间的百分比变化,长度比原序列少 1。


9. 构建二元趋势指标

有时需要二元指标而非连续值。示例如下,将连续价格变化转换为分类模型可用的趋势信号:

# 二元趋势(上涨为 1,下跌为 0)
trend_binary = (np.diff(values) > 0).astype(int)
print(trend_binary)

输出:

[1 1 1 0 1 1 0 0 1 0 0 1 0 0 1 0 1 0 0 1 0 1 0 1 1 0 1 0 1 1 1 0 0 1 0 1 0
 1 1 1 0 0 0 0 1 0 1 0 0 1 0 0 1 1 1 0 1 1 1 0 1 1 1 1 1 0 1 0 0 1 1 0 1 0
 1 0 0 0 0 1 1 1 0 1 1 1 1 1 1 1 1 0 1 1 0 1 1 0 0]

结果为一个二进制数组,表示相邻时间点的价格变动方向:上涨为 1,下跌为 0


10. 计算相关系数

在分析中常常需要计算变量之间的相关性。以下示例衡量价格波动与交易量之间的关系:

# 一行代码计算相关系数
price_volume_corr = np.corrcoef(stock_prices, volumes)[0, 1]
print(np.round(price_volume_corr,4))

输出:

0.5879

结果是一个介于 -1 和 1 之间的相关系数,表示线性关系的强度与方向。


总结

这些 NumPy 一行代码展示了如何利用向量化操作,让时间序列任务更高效、更简洁。它们涵盖了常见的实际问题,例如构建滞后特征、检测异常点以及计算金融统计指标,同时保持代码短小清晰。

真正的优势不仅在于代码简洁,还在于运行效率和可读性。NumPy 为性能而设计,这些操作能够很好地处理大规模数据集,并使代码结构更清晰易懂。

一旦掌握这些技巧,编写既高效又易于维护的时间序列代码将变得更加轻松。

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐