二、时序特征工程
1 多维时序特征分析Multi-Dimensional Temporal Feature Analysis
时序特征分析本身是一个很大的话题,在本次股票数据案例中,我们将从趋势分析特征、市场动态特征两个方面来进行时序特征分析。
股票市场的复杂和动态特性使得时序特征分析成为一个强大的工具。我们可以从历史数据中挖掘出时间序列数据的复杂模式,从而提取有价值的信息,助力构建更精准的预测模型。例如,如果分析揭示了一只股票在每个季度初都会上涨,投资者就可以利用这一信息在适当时期购买股票来获利。
时序特征分析不仅可以用于股票预测,还广泛应用于销售预测、天气预测、能源需求预测等许多领域。借助这一方法,我们能够从不断变化的时间序列数据中捕获核心趋势和模式,为建模和决策提供有力支持。
1.1 在线导入自定义库
1 | import pandas as pd |
1 | # 例如我们想要定义一个函数 |
kaggle中的notebook如何导入自己编写的特征衍生函数?
在机器学习的项目中,特征衍生是至关重要的一步。有时,我们可能会编写一些自定义的特征衍生函数。在本地环境中,导入这些函数相对简单,只需将.ipy文件和.py文件放在同一目录下即可。但在Kaggle的notebook环境中,事情就复杂了一些。下面将介绍如何在Kaggle上导入自己编写的特征衍生函数。
- 方法一:使用copyfile
-
上传文件:将自己编写的.py文件(例如名为features_creation.py)上传到Kaggle的input文件夹中。
-
导入copyfile模块:
1 | from shutil import copyfile |
- 复制文件到工作目录
1 | copyfile(src = "/kaggle/input/features-creation-jpx/features_creation.py", dst = "../working/features_creation.py") |
其中,src表示源文件的路径,dst表示目标路径,文件将被复制到此位置。
源文件路径需要复制自己数据集中的的.py文件
目标路径统一为…/working/ 也就是说你只需要改最后的文件名
- 导入模块:
1 | # 现在可以像导入其他模块一样导入自己编写的 py 文件了 |
- 方法二:直接编写代码
如果觉得上传过程较为繁琐,也可以直接在notebook中编写函数。这种方法适合于使用的函数不多的情况。
1 | def timeseries_feature(timeseris): |
1 | # 定义baseline中使用的adjust_price函数 |
1 | stock = pd.read_csv('/kaggle/input/jpx-tokyo-stock-exchange-prediction/train_files/stock_prices.csv') |
1.2 趋势分析特征
趋势分析是研究时间序列数据中变化趋势的核心方法。通过对数据的长期变化规律进行分析,我们能够揭示其潜在的发展方向,预测未来的走势,以及及时地检测到任何异常的变化点。
在机器学习领域,为了准确地捕捉和描述这些趋势信息,我们经常会采用以下几种技术方法:
历史平移:这种策略是将时间序列数据向后移动一定的时间间隔,与原始数据进行对比,从而得到趋势变化的方向和幅度。尽管这种方法简单而直观,但关键在于选择合适的平移间隔。若间隔选择得太小或太大,都可能导致捕捉到的趋势信息不准确。
增长率特征:此策略计算时间序列数据相邻两点之间的增长率或百分比变化率。通过观察增长率的正负和变化幅度,我们可以推测趋势的走向。但此方法可能会受到数据的大幅波动或异常值的影响,导致增长率估计不稳定。
窗口统计特征:窗口统计是一种常用的统计方法,它可以用来对时间序列数据进行分析。窗口统计的基本思想是将时间序列数据分割成若干个时间窗口,然后对每个时间窗口内的数据进行统计分析。根据窗口的类型,窗口统计可以分为跳动窗口、滑动窗口和扩展窗口(累计窗口)三种。这里我们主要介绍滑动窗扣和扩展窗口。
滑动窗口统计:此方法将时间序列数据划分为多个窗口,对每个窗口内的数据执行统计分析(例如计算均值、方差、最大值、最小值等),再根据这些特征值的变化来辨识趋势。使用这种方法可以降低数据中噪声的影响,但需要合适地选择窗口大小和步长,以避免信息的模糊或失真。
上图展示了滑动窗口统计的工作原理,窗口大小为2,步长为1,求窗口内的和作为结果。扩张窗口统计:该方法从时间序列数据的起始点出发,逐步扩大窗口范围,对每个扩张窗口内的数据进行统计分析。通过观察扩张窗口内的统计量如滚动统计量,我们可以获得对于数据整体变化趋势的深入洞察。尽管此方法能够充分利用历史数据,但选择合适的扩展步长同样关键,以确保计算效率和捕获到的趋势明确性。
上图解释了扩张窗口统计的原理,窗口范围从最开始逐渐扩展,步长为1,求每个扩展窗口的和。指数加权移动:指数加权移动平均(Exponentially Weighted Moving Average,EWMA)是一种应用于时间序列数据的权重平均技术。它为最近的数据赋予了更大的权重,而旧的数据权重则随时间指数衰减。因此,与简单移动平均相比,EWMA对于最近的观测值更为敏感,能够更快地反应数据的变化。
1.2.1 历史平移和增长率特征
为什么这两种特征在股票数据中适用?
在金融领域,人们相信"历史会重演"。虽然过去的表现不能完全预测未来,但历史数据提供了对市场趋势、周期和模式的深入了解,这些都是决策制定中不可或缺的。历史平移特征让我们能够直观地了解过去,而增长率特征则为我们提供了一个量化的方法,来测量股票在某段时间内的性能。
此外,投资者和分析师都知道,单靠当前股票的绝对价格是不足以做出投资决策的。他们需要了解股票的相对表现,即其与过去的比较。这就是为什么增长率和历史平移特征对于股票数据分析来说非常有价值的原因。
1 | # 本地可以运行下面的代码 |
- 历史平移特征
1 | # 我们以1301股票的前十条数据为例 |
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
| Open | Close | |
|---|---|---|
| 0 | 2734.0 | 2742.0 |
| 1865 | 2743.0 | 2738.0 |
| 3730 | 2734.0 | 2740.0 |
| 5595 | 2745.0 | 2748.0 |
| 7460 | 2748.0 | 2745.0 |
| 9325 | 2745.0 | 2731.0 |
| 11190 | 2707.0 | 2722.0 |
| 13055 | 2725.0 | 2704.0 |
| 14920 | 2702.0 | 2686.0 |
| 16785 | 2689.0 | 2694.0 |
1 | # 使用shift函数可以完成平移(和HomeCredit的操作类似) |
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
| Open | Close | history_close | |
|---|---|---|---|
| 0 | 2734.0 | 2742.0 | NaN |
| 1865 | 2743.0 | 2738.0 | 2742.0 |
| 3730 | 2734.0 | 2740.0 | 2738.0 |
| 5595 | 2745.0 | 2748.0 | 2740.0 |
| 7460 | 2748.0 | 2745.0 | 2748.0 |
| 9325 | 2745.0 | 2731.0 | 2745.0 |
| 11190 | 2707.0 | 2722.0 | 2731.0 |
| 13055 | 2725.0 | 2704.0 | 2722.0 |
| 14920 | 2702.0 | 2686.0 | 2704.0 |
| 16785 | 2689.0 | 2694.0 | 2686.0 |
1 | data.loc[:,'history_close'] = data['Close'].shift(2) |
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
| Open | Close | history_close | |
|---|---|---|---|
| 0 | 2734.0 | 2742.0 | NaN |
| 1865 | 2743.0 | 2738.0 | NaN |
| 3730 | 2734.0 | 2740.0 | 2742.0 |
| 5595 | 2745.0 | 2748.0 | 2738.0 |
| 7460 | 2748.0 | 2745.0 | 2740.0 |
| 9325 | 2745.0 | 2731.0 | 2748.0 |
| 11190 | 2707.0 | 2722.0 | 2745.0 |
| 13055 | 2725.0 | 2704.0 | 2731.0 |
| 14920 | 2702.0 | 2686.0 | 2722.0 |
| 16785 | 2689.0 | 2694.0 | 2704.0 |
解释:
history_close描述了一个时间窗口内的历史股票收盘价。通过平移数据,模型可以直观地看到某一天之前的股票价格,从而捕捉股票的历史趋势和模式。例如,shift(1)表示当前日期的1天前的收盘价。在金融领域,历史数据被广泛用来预测未来走势,因为股市的价格走势往往与其历史价格存在一定的关联。
- 增长率特征
1 | # pct_change(2)等价于(data['Close']-data['Close'].shift(2))/data['Close'].shift(2) |
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
| Open | Close | history_close | window_close_2_pctchange | |
|---|---|---|---|---|
| 0 | 2734.0 | 2742.0 | NaN | NaN |
| 1865 | 2743.0 | 2738.0 | NaN | NaN |
| 3730 | 2734.0 | 2740.0 | 2742.0 | -0.000729 |
| 5595 | 2745.0 | 2748.0 | 2738.0 | 0.003652 |
| 7460 | 2748.0 | 2745.0 | 2740.0 | 0.001825 |
| 9325 | 2745.0 | 2731.0 | 2748.0 | -0.006186 |
| 11190 | 2707.0 | 2722.0 | 2745.0 | -0.008379 |
| 13055 | 2725.0 | 2704.0 | 2731.0 | -0.009886 |
| 14920 | 2702.0 | 2686.0 | 2722.0 | -0.013226 |
| 16785 | 2689.0 | 2694.0 | 2704.0 | -0.003698 |
解释:
增长率特征描述了股票价格在一段时间内的变化率。它不仅考虑股票的绝对价格变化,还考虑到了相对于其初始价格的变化幅度,因此提供了更深入的洞察。增长率可以帮助投资者理解股价的动量和方向。正增长率表示价格上涨,而负增长率表示价格下跌。在金融分析中,这一特征非常重要,因为它可以帮助投资者捕捉到短期和中期的市场趋势。
和shift函数一样,我们需要适当的选择pct_change的时间。当pct_change中的参数为1,也就是计算收盘价隔天的增长率的时候,实际上计算的就是Target。(严格来说,根据官方的说明,是使用AdjustedClose计算的)
1 | import warnings |

1 | # 选取没有进行股价调整股票代码 |

只能看到一条线,因为两条线重合了
1.2.2 窗口统计特征
- 滑动窗口统计
滑动窗口是时间序列分析中的一个核心概念,它提供了一种在连续的时间点上分析数据的方法,同时保持了数据的局部特征。想象一下你正在用一块固定大小的玻璃逐步滑过一幅长图,每次移动都只看到玻璃下的那部分图像——这就是滑动窗口的基本思想。
在机器学习和统计分析中,我们使用滑动窗口来捕捉时间序列数据的短期模式或趋势。这通常涉及以下几个步骤:
- 选择窗口大小:这决定了每次观察的数据点数量。窗口的大小会影响到我们捕捉到的数据模式——太小的窗口可能会过度关注噪声,而太大的窗口可能会丧失细节。
- 选择步长:步长决定了窗口每次移动的数据点数量。步长较小会产生更多的重叠,可以更精细的分析,但计算量也会增大。
- 应用统计函数:对每个窗口内的数据应用统计函数(如均值、中位数、方差等),从而提取出有用的特征或信息。
滑动窗口的技术不仅可以用来提取特征,还可以用于数据的平滑、异常检测以及预测等多种任务,使其成为时间序列分析的强大工具。
这里我们重点介绍滑动窗口求平均值以及波动比率特征的介绍:
滑动窗口计算平均值特征
1 | data['Close'] |
0 2742.0
1865 2738.0
3730 2740.0
5595 2748.0
7460 2745.0
9325 2731.0
11190 2722.0
13055 2704.0
14920 2686.0
16785 2694.0
Name: Close, dtype: float64
1 | # pd.Series.rolling即可完成滑窗操作 |
0 NaN
1865 2740.0
3730 2739.0
5595 2744.0
7460 2746.5
9325 2738.0
11190 2726.5
13055 2713.0
14920 2695.0
16785 2690.0
Name: Close, dtype: float64
1 | pd.__version__ |
'1.3.5'
1 | # 此版本rolling不支持自定义步长的大小 |
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
~\AppData\Local\Temp\ipykernel_23048\821644658.py in <module>
1 # 此版本rolling不支持自定义步长的大小
2 # pandas版本低于1.5.0就不能自定义步长的大小
----> 3 data['Close'].rolling(window=2,step=2).mean()
TypeError: rolling() got an unexpected keyword argument 'step'
1 | # 有时候我们需要在同一个窗口下同时计算多个统计量,则可以借助agg函数来完成 |
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
| Open | Close | history_close | window_close_2_pctchange | window_close_5_mean | window_close_5_std | window_close_5_max | window_close_5_min | window_close_5_median | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 2734.0 | 2742.0 | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
| 1865 | 2743.0 | 2738.0 | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
| 3730 | 2734.0 | 2740.0 | 2742.0 | -0.000729 | NaN | NaN | NaN | NaN | NaN |
| 5595 | 2745.0 | 2748.0 | 2738.0 | 0.003652 | NaN | NaN | NaN | NaN | NaN |
| 7460 | 2748.0 | 2745.0 | 2740.0 | 0.001825 | 2742.6 | 3.974921 | 2748.0 | 2738.0 | 2742.0 |
| 9325 | 2745.0 | 2731.0 | 2748.0 | -0.006186 | 2740.4 | 6.580274 | 2748.0 | 2731.0 | 2740.0 |
| 11190 | 2707.0 | 2722.0 | 2745.0 | -0.008379 | 2737.2 | 10.663020 | 2748.0 | 2722.0 | 2740.0 |
| 13055 | 2725.0 | 2704.0 | 2731.0 | -0.009886 | 2730.0 | 17.958285 | 2748.0 | 2704.0 | 2731.0 |
| 14920 | 2702.0 | 2686.0 | 2722.0 | -0.013226 | 2717.6 | 23.093289 | 2745.0 | 2686.0 | 2722.0 |
| 16785 | 2689.0 | 2694.0 | 2704.0 | -0.003698 | 2707.4 | 18.836135 | 2731.0 | 2686.0 | 2704.0 |
【思考】:为什么滑动窗口统计能产生显著的效果?
滑动窗口统计是时间序列分析中的常见手段。它的主要原理是对数据中的每一段子集(即窗口)进行计算,以提取数据的某些特性。这样做可以帮助我们更好地理解数据在某个特定时间段的特点。例如,通过计算移动平均,我们可以得到数据的局部平均值,从而捕捉到数据的中期趋势,而不受到短期波动的干扰。
考虑股票1301,当我们使用滑动窗口技术绘制移动平均线(MA)时:
1 | import seaborn as sns |
观察图像,我们会发现移动平均线比实际股价走势要更加平滑。这种平滑效果在窗口大小增大时会更加明显。因此,移动平均线实际上捕捉到了数据的长期趋势特性,并在一定程度上消除了数据的短期波动。
打开任何股票软件,通常我们都可以看到某只股票的5日、10日、20日移动平均线(MA):

在股票市场中,这些移动平均线之间的交叉具有特殊的意义。例如,短期均线下穿长期均线时,这种现象被称为“死叉”,通常被视为卖出的信号;相反,短期均线上穿长期均线时,被称为“金叉”,通常被视为买入的信号。然而,值得注意的是,虽然这些交叉提供了一些参考信息,但它们仅仅是技术分析中的一种工具,并不应被视为绝对的买卖准则。实际的投资决策需要综合多种因素,并考虑个人的投资策略和风险承受能力。
滑动窗口计算极值特征(Max-Min Feature)和波动比率
极值主要指的是某个特定时间段内股票价格的最高和最低值。对于任何一段时间(例如一天、一周或一个月),极值可以理解为这段时间内市场的最高和最低预期。这可以帮助我们了解市场在这段时间内的市场活跃度、潜在的阻力和支撑水平(心理关卡)和风险预估。
极值特征示例代码:
1 | # 创建示例数据集 |
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
| Date | Close | |
|---|---|---|
| 0 | 2017-01-04 | 2742.0 |
| 1865 | 2017-01-05 | 2738.0 |
| 3730 | 2017-01-06 | 2740.0 |
| 5595 | 2017-01-10 | 2748.0 |
| 7460 | 2017-01-11 | 2745.0 |
| 9325 | 2017-01-12 | 2731.0 |
| 11190 | 2017-01-13 | 2722.0 |
| 13055 | 2017-01-16 | 2704.0 |
| 14920 | 2017-01-17 | 2686.0 |
| 16785 | 2017-01-18 | 2694.0 |
1 | # 创建一个10天时间窗口的最大值、最小值和差值特征 |
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
| Date | Close | window_close_5_max | window_close_5_min | window_close_5_max-min | |
|---|---|---|---|---|---|
| 0 | 2017-01-04 | 2742.0 | NaN | NaN | NaN |
| 1865 | 2017-01-05 | 2738.0 | NaN | NaN | NaN |
| 3730 | 2017-01-06 | 2740.0 | NaN | NaN | NaN |
| 5595 | 2017-01-10 | 2748.0 | NaN | NaN | NaN |
| 7460 | 2017-01-11 | 2745.0 | 2748.0 | 2738.0 | 10.0 |
| 9325 | 2017-01-12 | 2731.0 | 2748.0 | 2731.0 | 17.0 |
| 11190 | 2017-01-13 | 2722.0 | 2748.0 | 2722.0 | 26.0 |
| 13055 | 2017-01-16 | 2704.0 | 2748.0 | 2704.0 | 44.0 |
| 14920 | 2017-01-17 | 2686.0 | 2745.0 | 2686.0 | 59.0 |
| 16785 | 2017-01-18 | 2694.0 | 2731.0 | 2686.0 | 45.0 |
波动比率
在时序数据中,特别是股票价格数据,单纯地考虑价格的波动值可能不够充分,因为股票的价格在不同时间可能有很大的差异(比如,有的股票在过去的价位很低,现在价位很高)。在这种情况下,固定的波动值对于低价股票可能非常显著,但对于高价股票则可能不那么显著。因此,考虑波动的比率是有意义的。其计算公式:
1 | # 使用平均值为基准计算波动比率 |
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
| Date | Close | window_close_5_max | window_close_5_min | window_close_5_max-min | window_close_5_max-min_ratio_avg | |
|---|---|---|---|---|---|---|
| 0 | 2017-01-04 | 2742.0 | NaN | NaN | NaN | NaN |
| 1865 | 2017-01-05 | 2738.0 | NaN | NaN | NaN | NaN |
| 3730 | 2017-01-06 | 2740.0 | NaN | NaN | NaN | NaN |
| 5595 | 2017-01-10 | 2748.0 | NaN | NaN | NaN | NaN |
| 7460 | 2017-01-11 | 2745.0 | 2748.0 | 2738.0 | 10.0 | 0.003646 |
| 9325 | 2017-01-12 | 2731.0 | 2748.0 | 2731.0 | 17.0 | 0.006206 |
| 11190 | 2017-01-13 | 2722.0 | 2748.0 | 2722.0 | 26.0 | 0.009506 |
| 13055 | 2017-01-16 | 2704.0 | 2748.0 | 2704.0 | 44.0 | 0.016141 |
| 14920 | 2017-01-17 | 2686.0 | 2745.0 | 2686.0 | 59.0 | 0.021727 |
| 16785 | 2017-01-18 | 2694.0 | 2731.0 | 2686.0 | 45.0 | 0.016614 |
- 扩展窗口统计
在前面的部分,我们详细探讨了滑动窗口统计的原理及其应用。滑动窗口通过定义固定的时间段来计算时间序列数据中的统计量,为我们提供了对数据局部特性的深入了解。现在,我们将转向另一种强大的工具:扩展窗口统计。
扩展窗口统计与滑动窗口统计在许多方面都具有相似性,它们都侧重于计算窗口内的统计量来揭示数据的内在特性。然而,扩展窗口的一个显著特点是其动态的窗口大小。与滑动窗口的固定大小不同,扩展窗口的大小会随着新数据的加入而逐渐扩大。换句话说,它始终从序列的起始点开始,但其结束点会随着时间而移动,从而包含更多的数据。
因此,扩展窗口有时也被称为累计窗口。这是因为它累计了从时间序列的开始到当前时间点的所有数据,从而提供了一种全面的视角。
在金融领域,扩展窗口的概念尤为重要。例如,证券的持有期收益率称为持有收益率(Holding Period Return)。这是指从投资者开始持有某种证券到某个特定时间点的累计收益。其值不仅仅是单一时间段内的收益,而是从购买之日起至今的总收益。
通过对这种持有期收益率的分析,投资者可以更好地评估其投资策略的长期有效性,而不仅仅是短期的表现。这为金融分析师和投资者提供了一个宝贵的工具,帮助他们在复杂的金融市场中做出明智的决策。
1 | # 扩展窗口的实现代码也很简单,只需要pd.Series.expanding()即可,后面加上自己需要计算的统计量 |
0 2742.000000
1865 2740.000000
3730 2740.000000
5595 2742.000000
7460 2742.600000
9325 2740.666667
11190 2738.000000
13055 2733.750000
14920 2728.444444
16785 2725.000000
Name: Close, dtype: float64
1 | # 我们把扩展窗口计算的均值赋值给新列 |
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
| Date | Close | window_close_5_max | window_close_5_min | window_close_5_max-min | window_close_5_max-min_ratio_avg | expand_mean | |
|---|---|---|---|---|---|---|---|
| 0 | 2017-01-04 | 2742.0 | NaN | NaN | NaN | NaN | 2742.000000 |
| 1865 | 2017-01-05 | 2738.0 | NaN | NaN | NaN | NaN | 2740.000000 |
| 3730 | 2017-01-06 | 2740.0 | NaN | NaN | NaN | NaN | 2740.000000 |
| 5595 | 2017-01-10 | 2748.0 | NaN | NaN | NaN | NaN | 2742.000000 |
| 7460 | 2017-01-11 | 2745.0 | 2748.0 | 2738.0 | 10.0 | 0.003646 | 2742.600000 |
| 9325 | 2017-01-12 | 2731.0 | 2748.0 | 2731.0 | 17.0 | 0.006206 | 2740.666667 |
| 11190 | 2017-01-13 | 2722.0 | 2748.0 | 2722.0 | 26.0 | 0.009506 | 2738.000000 |
| 13055 | 2017-01-16 | 2704.0 | 2748.0 | 2704.0 | 44.0 | 0.016141 | 2733.750000 |
| 14920 | 2017-01-17 | 2686.0 | 2745.0 | 2686.0 | 59.0 | 0.021727 | 2728.444444 |
| 16785 | 2017-01-18 | 2694.0 | 2731.0 | 2686.0 | 45.0 | 0.016614 | 2725.000000 |
1.2.3 指数加权移动平均(EWMA)
指数加权移动平均(EWMA)是时间序列数据平滑方法之一,尤其在金融和经济领域中具有广泛的应用。与简单移动平均不同,EWMA为数据中的每个观测值分配不同的权重。这种权重分配是按照指数递减的方式进行的,这意味着近期的数据点会被赋予较高的权重,而较早的数据点权重则逐渐降低。与简单移动平均线(SMA)相比,EWMA对最近的价格变化更为敏感。这意味着,当市场出现新的信息或变化时,EWMA能够更快地反映这些变化。
其计算公式为:
其中, 是在时间点t的观测值, 是在时间点t-1的指数加权移动平均值, 是权重因子,范围在0到1之间。
- 当接近1时,表示我们更多地依赖最近的观测值。这使得EWMA对近期变化更加敏感,但也可能引入更多的噪声。
- 而当接近0时,模型将更加依赖过去的平均值,从而使得EWMA更加平稳。但这也可能使其对突发的变化反应迟钝。
在实际应用中,选择合适的 值至关重要。这通常需要根据具体的应用场景和数据特性进行选择。例如,金融市场中的股票价格可能会频繁波动,因此可能需要一个较高的 值来捕捉这些短期变化。而在其他更为稳定的时间序列数据中,较低的 值可能更为合适。
总的来说,EWMA提供了一种灵活的方式来平滑时间序列数据,同时考虑到了数据中的时间依赖性。通过合适地选择权重因子,它可以为分析师和决策者提供有关数据趋势和模式的有价值的洞察。
1 | # pd.Series.ewm 可以实现指数加权的计算,在后面加上.mean()就可以计算指数加权平均 |
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
| Date | Close | window_close_5_max | window_close_5_min | window_close_5_max-min | window_close_5_max-min_ratio_avg | expand_mean | ewma | |
|---|---|---|---|---|---|---|---|---|
| 0 | 2017-01-04 | 2742.0 | NaN | NaN | NaN | NaN | 2742.000000 | 2742.000000 |
| 1865 | 2017-01-05 | 2738.0 | NaN | NaN | NaN | NaN | 2740.000000 | 2740.800000 |
| 3730 | 2017-01-06 | 2740.0 | NaN | NaN | NaN | NaN | 2740.000000 | 2740.560000 |
| 5595 | 2017-01-10 | 2748.0 | NaN | NaN | NaN | NaN | 2742.000000 | 2742.792000 |
| 7460 | 2017-01-11 | 2745.0 | 2748.0 | 2738.0 | 10.0 | 0.003646 | 2742.600000 | 2743.454400 |
| 9325 | 2017-01-12 | 2731.0 | 2748.0 | 2731.0 | 17.0 | 0.006206 | 2740.666667 | 2739.718080 |
| 11190 | 2017-01-13 | 2722.0 | 2748.0 | 2722.0 | 26.0 | 0.009506 | 2738.000000 | 2734.402656 |
| 13055 | 2017-01-16 | 2704.0 | 2748.0 | 2704.0 | 44.0 | 0.016141 | 2733.750000 | 2725.281859 |
| 14920 | 2017-01-17 | 2686.0 | 2745.0 | 2686.0 | 59.0 | 0.021727 | 2728.444444 | 2713.497301 |
| 16785 | 2017-01-18 | 2694.0 | 2731.0 | 2686.0 | 45.0 | 0.016614 | 2725.000000 | 2707.648111 |
当adjust=True时,指数加权移动平均值(EWMA)是使用权重计算的。例如,序列的EWMA值为:
其中,是ewm函数的一个参数,用于控制EWMA值对新数据的敏感程度。的取值范围为,和上面一样,越大,EWMA值对新数据的敏感程度越高。
当adjust=True时,EWMA值更接近于原始数据;而当adjust=False时,EWMA值更平滑:
1 | import pandas as pd |
.dataframe thead th {
text-align: right;
}
| Close | ewma_adjust_true | ewma_adjust_false | |
|---|---|---|---|
| 0 | 100 | 100.000000 | 100.0000 |
| 1 | 101 | 100.666667 | 100.5000 |
| 2 | 102 | 101.428571 | 101.2500 |
| 3 | 103 | 102.266667 | 102.1250 |
| 4 | 104 | 103.161290 | 103.0625 |
上面我们只是计算了指数加权平均,同样的,我们还可以计算其他统计量:
- .mean():计算指数加权移动平均值。
- .var():计算指数加权移动方差。
- .std():计算指数加权移动标准差。这实际上是 .var() 的平方根。
- .corr():计算两组数的指数加权移动相关系数。
- .cov():计算两组数的指数加权移动协方差。
上面我们是直接指定了的取值,但是它也可以通过别的参数来指定:
EWM (指数加权移动) 参数详解
- 衰减因子选择:
选择权重衰减速率时,必须指定以下参数中的一个:
- alpha:直接指定平滑因子 α,范围为 0 < α ≤ 1。
- com(center of mass):指定衰减的中心质量(物理学概念)。通过公式 计算,其中 com ≥ 0。
- halflife:指定半衰期(应用于化学和核物理学)。通过公式计算,其中 halflife > 0。
- span:指定衰减的跨度(数学和统计学概念)。通过公式 计算,其中 span ≥ 1。股票数据使用span参数的情况会比较多一些。
-
为什么选择span参数?
在金融领域,分析师和交易员常参考固定天数的移动平均,如10天、30天、50天。但简单的移动平均可能忽略短期的重要变化。EWMA,特别是通过设置span,能够更精确地反映数据变化。-
跨度的意义:例如,“30天的跨度”(span=30)代表权重衰减的基本长度为30天。30天前的数据权重是当前数据的一半。超过30天的数据虽被考虑,但权重迅速衰减。
-
跨度与其他移动平均的对比:span参数为EWMA提供了直观的定义方式,方便与简单移动平均线(SMA)等进行比较。(所以代码实现也会更直接和简单,可以指定同一个period)
-
-
其他参数:
-
min_periods:窗口中所需的最小观测值数量。默认值为 0。少于此数量则结果为 np.nan。
-
ignore_na:是否在计算权重时忽略缺失值。默认为 False。
-
axis:指定计算的方向。默认为 0(或 ‘index’)。若为 0 或 ‘index’,则沿行计算;若为 1 或 ‘columns’,则沿列计算。
-
times 参数:与观测值对应的时间。必须单调递增且具为
datetime类型。如果指定了times参数之后halftime则表示观测值衰减到其一半值所需的时间单位(仅计算均值时生效)
-
1 | # 使用times参数有比较多的限制,所以我们创建一个示例数据来进行说明 |
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
| Date | Value | |
|---|---|---|
| 0 | 2022-01-01 | 1 |
| 1 | 2022-01-02 | 2 |
| 2 | 2022-01-03 | 3 |
| 3 | 2022-01-04 | 4 |
| 4 | 2022-01-05 | 5 |
1 | data['Value'].ewm(span=5,adjust=False).mean() |
0 1.000000
1 1.333333
2 1.888889
3 2.592593
4 3.395062
Name: Value, dtype: float64
1 | # 首先times必须是时间戳数据 |
0 1.000000
1 1.534602
2 2.092125
3 2.672350
4 3.274976
Name: Value, dtype: float64
1 | # 直接指定alpha也不行 |
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
~\AppData\Local\Temp\ipykernel_23048\2193549492.py in <module>
1 # 直接指定alpha也不行
----> 2 data['Value'].ewm(times=times,alpha=0.1).mean()
c:\ProgramData\Anaconda3\envs\ai\lib\site-packages\pandas\core\generic.py in ewm(self, com, span, halflife, alpha, min_periods, adjust, ignore_na, axis, times)
10928 ignore_na=ignore_na,
10929 axis=axis,
> 10930 times=times,
10931 )
10932
c:\ProgramData\Anaconda3\envs\ai\lib\site-packages\pandas\core\window\ewm.py in __init__(self, obj, com, span, halflife, alpha, min_periods, adjust, ignore_na, axis, times, selection)
306 if not isinstance(self.halflife, (str, datetime.timedelta)):
307 raise ValueError(
--> 308 "halflife must be a string or datetime.timedelta object"
309 )
310 if isna(self.times).any():
ValueError: halflife must be a string or datetime.timedelta object
1 | # 而且adjust也只能设置为True,即使用默认值 |
---------------------------------------------------------------------------
NotImplementedError Traceback (most recent call last)
~\AppData\Local\Temp\ipykernel_23048\2431065888.py in <module>
1 # 而且adjust也只能设置为True,即使用默认值
----> 2 data['Value'].ewm(times=times,halflife='5D',adjust=False).mean()
c:\ProgramData\Anaconda3\envs\ai\lib\site-packages\pandas\core\generic.py in ewm(self, com, span, halflife, alpha, min_periods, adjust, ignore_na, axis, times)
10928 ignore_na=ignore_na,
10929 axis=axis,
> 10930 times=times,
10931 )
10932
c:\ProgramData\Anaconda3\envs\ai\lib\site-packages\pandas\core\window\ewm.py in __init__(self, obj, com, span, halflife, alpha, min_periods, adjust, ignore_na, axis, times, selection)
295 if self.times is not None:
296 if not self.adjust:
--> 297 raise NotImplementedError("times is not supported with adjust=False.")
298 if isinstance(self.times, str):
299 self.times = self._selected_obj[self.times]
NotImplementedError: times is not supported with adjust=False.
1.3 市场动态特征
在分析股票数据时,除了研究价格的长期趋势之外,还需要关注其短期的动态变化。这就是所谓的“市场动态特征”。与“趋势分析特征”主要关注价格的整体走势、形成的长期模式不同,“市场动态特征”更加注重短期内的价格波动、变动范围和特定的市场行为模式。通过对这些特征的分析,我们可以更加细致地了解市场在某一特定时间段内的行为,从而做出更为精准的预测和决策。
接下来,我们将详细探讨这部分的三个核心特征:
- 差异和波动性特征则展现了价格的不稳定性,帮助我们感知市场的风险;
- 阴阳线特征通过研究开盘和收盘价的关系,为我们揭示了市场的买卖力量分布;
- 基础时间特征则将时间因素与价格行为结合,使模型能够理解某些周期性的市场行为,捕捉数据的时间属性和规律。
- 差异和波动性特征(Diff and Volatility Features)
差异特征 diff 表示在特定的时间窗口内股价的绝对变化,为交易策略提供方向。
1 | data = df_stock[df_stock['SecuritiesCode']==1301][['Date','Close']].head(10) |
.dataframe thead th {
text-align: right;
}
| Date | Close | close_diff_5 | |
|---|---|---|---|
| 0 | 2017-01-04 | 2742.0 | NaN |
| 1865 | 2017-01-05 | 2738.0 | NaN |
| 3730 | 2017-01-06 | 2740.0 | NaN |
| 5595 | 2017-01-10 | 2748.0 | NaN |
| 7460 | 2017-01-11 | 2745.0 | NaN |
| 9325 | 2017-01-12 | 2731.0 | -11.0 |
| 11190 | 2017-01-13 | 2722.0 | -16.0 |
| 13055 | 2017-01-16 | 2704.0 | -36.0 |
| 14920 | 2017-01-17 | 2686.0 | -62.0 |
| 16785 | 2017-01-18 | 2694.0 | -51.0 |
上面的操作实际上是叫做一阶差分,相当于data - data.shift(5),那么这种简单的作差操作,为什么就可以帮助模型理解数据并建模呢?
一阶差分和二阶差分是时间序列分析中常用的技术,主要用于使时间序列数据变得更加平稳,即减少或消除数据中的趋势和季节性成分。数据的平稳性是许多时间序列模型,如ARIMA模型,的一个基本假设。大多数的时间序列模型都要求输入的数据是平稳的,或者至少是宽度平稳的。当数据存在趋势或季节性时,模型可能会被这些趋势所迷惑,导致预测不准确。差分技术可以帮助消除这些趋势和季节性,从而使得数据变得平稳。
- 一阶差分:
- 一阶差分可以帮助移除线性的趋势。
- 对于时间序列数据,一阶差分表示为连续观测值之间的差异。
- 例如,如果我们有一个上升的线性趋势,一阶差分可能会使得这个趋势消失,从而得到一个平稳的时间序列。
- 二阶差分:
- 当一阶差分无法消除时间序列中的趋势时,我们可能会尝试二阶差分。
- 二阶差分有助于消除曲线的趋势。
对于原始数据、一阶差分和二阶差分之间的关系可以对照高中物理的位移、速度、加速度之间的关系。通过差分技术,我们可以获得一个更为平稳、可解释且与模型假设相一致的时间序列数据,增加连续观测值之间的相关性,从而帮助模型更好地理解数据和进行预测。
波动性特征:帮助评估市场的不确定性或风险,波动性增加可能预示大的价格变动,其本质是衡量价格或收益率在某段时间内的变化程度,因此任何可以衡量变化幅度的统计工具理论上都可以用来计算波动性。在方案中,我们计算的是对数收益的滚动标准差。
这个波动性和上面滑动窗口板块计算的波动比率有什么联系和区别?
- 它们都是描述股票价格波动性的方法,只是从不同的角度和计算方式来进行描述。
- 波动比率使用了最大值和最小值来描述股价在一段时间内的波动范围,而对数波动性则使用了价格的对数日回报率的标准差来描述价格的变化程度。
- 在实际的金融分析中,这两种方法都可以用来估计股票的风险和潜在回报,投资者和分析师可以结合这两种方法来更全面地了解股票的价格动态。
指数收益(对数日回报率)的计算公式:
其中,为时间 的价格,为时间 的价格
1 | # 用代码来表示就是 |
0 NaN
1865 -0.001460
3730 0.000730
5595 0.002915
7460 -0.001092
9325 -0.005113
11190 -0.003301
13055 -0.006635
14920 -0.006679
16785 0.002974
Name: Close, dtype: float64
1 | # 考虑到指数的运算规则,也可以使用差分函数diff来实现 |
0 NaN
1865 -0.001460
3730 0.000730
5595 0.002915
7460 -0.001092
9325 -0.005113
11190 -0.003301
13055 -0.006635
14920 -0.006679
16785 0.002974
Name: Close, dtype: float64
基于上述对数日回报率,接下来使用窗口函数计算对数收益率的滚动标准差,代表价格的对数波动性:
1 | # 这里我们设置窗口为5 |
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
| Date | Close | close_diff_5 | log_diff | logvolatility_5day | |
|---|---|---|---|---|---|
| 0 | 2017-01-04 | 2742.0 | NaN | NaN | NaN |
| 1865 | 2017-01-05 | 2738.0 | NaN | -0.001460 | NaN |
| 3730 | 2017-01-06 | 2740.0 | NaN | 0.000730 | NaN |
| 5595 | 2017-01-10 | 2748.0 | NaN | 0.002915 | NaN |
| 7460 | 2017-01-11 | 2745.0 | NaN | -0.001092 | NaN |
| 9325 | 2017-01-12 | 2731.0 | -11.0 | -0.005113 | 0.002969 |
| 11190 | 2017-01-13 | 2722.0 | -16.0 | -0.003301 | 0.003178 |
| 13055 | 2017-01-16 | 2704.0 | -36.0 | -0.006635 | 0.003734 |
| 14920 | 2017-01-17 | 2686.0 | -62.0 | -0.006679 | 0.002383 |
| 16785 | 2017-01-18 | 2694.0 | -51.0 | 0.002974 | 0.004005 |
这个表达式通常用于计算金融时间序列数据中收盘价的波动性,因为标准差是衡量数据分布的统计量,能够反映数据的离散程度。因此,在金融分析中,标准差通常用来反映价格的波动性,波动性越大,也就意味着股票的变化幅度越大,风险越高。
【扩展】: 非金融专业的角度该如何理解“波动性”?上面我们计算的是指数收益的波动性,简单收益率不行吗?为什么要使用对数收益率?
- 波动性简介:波动性其实就是描述收益率的变化幅度有多大。试想一下,如果一个股票的价格每天都上涨1%,那它的变化就很稳定,波动性低。但如果价格时而大涨,时而大跌,那它的波动性就很高了。而这种变化幅度在数学上,我们用“标准差”来量化这种波动性。(回想我们在夏普比率中用到的分母,那其实也是一个波动性的指标)。
- 简单收益率与对数收益率的选择:
简单收益率:
从直观上理解,它描述了价格从一个时间点到另一个时间点的变化情况。从公式的理解上来说,简单收益率确实有可取之处。因此,尽管在解释实际的经济意义的时候我们会使用简单收益率,但是在对金融数据进行衍生的时候我们还是会优先考虑对数收益率,因为其优秀的数学理质:
- 加性属性:多个连续时期的对数收益率可以简单地累加,对于模型来说,比较容易捕捉到这种信息
- 数学和统计特性:对数收益率在许多统计和金融模型中都显示出了更好的数学特性,因为对数收益率的分布往往更接近正态分布(许多金融理论的基本假设前提都是服从随机的正态分布)。
- 数值稳定性:对于较小的收益值,对数收益率与简单收益率之间的差异其实并不大。这使得对数收益率更为稳定,特别适用于金融数据的时间序列分析。所以,尽管在解释实际投资回报时我们可能会用简单收益率,但在构建金融模型时,对数收益率往往是首选。
波动性特征并不仅仅适用于股票数据。事实上,波动性是一个广泛的概念,可以用来描述许多不同类型数据的不稳定性或变化幅度。任何可能随时间变化、并且我们希望衡量其变化程度的数据都可以考虑计算其波动性。例如外汇市场、商品市场(油价金价农产品价格)、利率、房地产市场、流量和使用率、气候和天气数据、电力和能源消耗、销售和供应链数据等等。只要存在变化和不确定性的地方,波动性度量就可能是一个有用的有效的衍生方向。
- 阴阳线特征(Candlestick Features)
股票行情的“K线图”
以篮球比赛为例,比赛的每一分、每一秒都充满了紧张和刺激,但我们通常只关心得分、助攻、篮板等关键数据。同理,股票的价格每天都在变动,但我们可以通过K线图来捕捉每天的四个关键价格,其由若干个阴线和阳线构成,红色的是阳线,绿色的阴线:

以单日K线图为例,每根K线图都包含了四个价格信息:开盘价、收盘价、最高价和最低价,中间的柱子叫做实体部分,实体部分可以是空白的,也可以是红色或绿色填充的。实体上面叫做上影线,在阳线中表示当日最高价和收盘价之间的差值(如果是阴线的话就是开盘价),下影线同理。

在这部分,上影线和下影线可以作为一个技术指标:
在K线图中,长上影线和长下影线确实常常被解读为市场的不确定性和多空双方的斗争(多指的是做多的一方,空指的是做空的一方)。总之,影线的长短可以反映买卖双方的交易激烈程度(市场情绪)。当影线很长时,说明当日股票价格波动较大,买卖双方交易激烈。
我们可以使用下面的代码计算K线图中的上影线和下影线长度:
1 | data = df_stock[df_stock['SecuritiesCode']==1301][['Date','Open','Close','High','Low']].head(10) |
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
| Date | Open | Close | High | Low | Upper_Shadow | Lower_Shadow | |
|---|---|---|---|---|---|---|---|
| 0 | 2017-01-04 | 2734.0 | 2742.0 | 2755.0 | 2730.0 | 13.0 | 4.0 |
| 1865 | 2017-01-05 | 2743.0 | 2738.0 | 2747.0 | 2735.0 | 4.0 | 3.0 |
| 3730 | 2017-01-06 | 2734.0 | 2740.0 | 2744.0 | 2720.0 | 4.0 | 14.0 |
| 5595 | 2017-01-10 | 2745.0 | 2748.0 | 2754.0 | 2735.0 | 6.0 | 10.0 |
| 7460 | 2017-01-11 | 2748.0 | 2745.0 | 2752.0 | 2737.0 | 4.0 | 8.0 |
| 9325 | 2017-01-12 | 2745.0 | 2731.0 | 2747.0 | 2703.0 | 2.0 | 28.0 |
| 11190 | 2017-01-13 | 2707.0 | 2722.0 | 2730.0 | 2707.0 | 8.0 | 0.0 |
| 13055 | 2017-01-16 | 2725.0 | 2704.0 | 2725.0 | 2696.0 | 0.0 | 8.0 |
| 14920 | 2017-01-17 | 2702.0 | 2686.0 | 2704.0 | 2682.0 | 2.0 | 4.0 |
| 16785 | 2017-01-18 | 2689.0 | 2694.0 | 2695.0 | 2681.0 | 1.0 | 8.0 |
K线图也被称为是蜡烛图,除了股票数据,蜡烛图也常用于分析其他金融产品,如外汇、债券、商品等。这种计算影线的衍生方法适用于任何具有与时间相关的最高值、最低值、开始值和结束值的数据。例如,一家公司在一个月内的日销售额,其中开盘价和收盘价可以分别被视为当天的起始销售额和结束销售额。虽然影线的计算常常与股票和其他金融产品关联,但其背后的逻辑和分析方法可以应用于任何具有开、高、低、闭四个关键数据点的情境中。
- 基础时间特征(Time Features)
基础时间特征是从时间序列中数据点的时间戳派生的简单特征。可以使用下面的 timeseries_feature 函数来实现
1 | def timeseries_feature(timeseris): |
通过上面的timeseries_feature函数可以为数据衍生出如周、月、季度、年等时间单位特征,这对于分析季节性和其他基于时间的模式非常有帮助,具体体现在:
-
年、月、日:这三个特征可以帮助模型理解数据中的长期趋势、季节性变化以及一个月中某些日子可能存在的模式。例如,零售业销售数据可能会在特定的月份、季度或某些日期(比如黑色星期五)上升。
-
季度:许多业务活动都是按季度进行的,例如公司的季度报告。这个特征可以帮助模型捕捉这些季度效应。
-
一年中的第几周:这可以捕捉到一年中某些周的特定模式,例如假期购物季。
-
一周中的第几天:某些事件或模式可能在一周的某些天出现,比如周末或周中。例如,周五的电影票销售可能会比周三的多。
-
是否为周末:与“一周中的第几天”类似,但更加明确地标注了是否为周末。对于一些业务,周末可能与工作日有非常大的不同,例如餐馆的就餐量或旅游景点的参观量。(股票在周末和节假日不会交易,所以注释掉了)
看起来如此基础的时间戳衍生,为什么能够有效果?在哪些场景下可能会有何种效果?
-
季节性:许多现象都会受到季节性的影响,例如零售销售通常在节假日期间增加,而能源需求可能在冬季或夏季达到高峰。通过提取月份或季度信息,我们可以帮助模型捕捉到这些季节性趋势。
-
趋势分析:提取年份可以帮助模型识别长期趋势。例如,一个产业的产量可能随着时间的推移而逐渐增长或减少。
-
周内效应:一些现象在一周中的不同天有不同的表现。例如,餐馆在周末可能比工作日更加忙碌,股票市场在周一可能比其他工作日有不同的行为。通过提取“dayofweek”特征,我们可以捕捉到这种周期性变化。
-
特定事件:有时某些特定的日期或月份可能与特定的事件或活动相关联。例如,学校的开学月、大型促销或节假日。这些日期可能会对目标变量产生影响,因此提取这些基本特征可能很有用。
-
模型要求提供周期性因子:一些模型,如Facebook的Prophet,要求明确地提供日、周和年的周期性信息来帮助预测。
-
为交叉衍生作准备:时间特征也可以与其他特征组合来创建交叉特征。例如,将月份和商品类别组合,可以帮助模型更好地理解在特定月份哪些商品的销售可能会增加。
基础时间特征的衍生在许多项目中都是标准的第一步,因为它们通常能为模型提供大量有关数据的上下文信息。
1.4 应用于当前数据集
首先将上述时序特征工程手段封装在一个函数里面
1 | def create_feature(df_temp:pd.DataFrame,periods:Union[List,Tuple,Iterable])->pd.DataFrame: |
1 | stock = pd.read_csv('/kaggle/input/jpx-tokyo-stock-exchange-prediction/train_files/stock_prices.csv') |
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
| SecuritiesCode | EffectiveDate | Name | Section/Products | NewMarketSegment | 33SectorCode | 33SectorName | 17SectorCode | 17SectorName | NewIndexSeriesSizeCode | NewIndexSeriesSize | TradeDate | Close | IssuedShares | MarketCapitalization | Universe0 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1301 | 20211230 | KYOKUYO CO.,LTD. | First Section (Domestic) | Prime Market | 50 | Fishery, Agriculture and Forestry | 1 | FOODS | 7 | TOPIX Small 2 | 20211230.0 | 3080.0 | 1.092828e+07 | 3.365911e+10 | True |
| 1 | 1305 | 20211230 | Daiwa ETF-TOPIX | ETFs/ ETNs | NaN | - | - | - | - | - | - | 20211230.0 | 2097.0 | 3.634636e+09 | 7.621831e+12 | False |
| 2 | 1306 | 20211230 | NEXT FUNDS TOPIX Exchange Traded Fund | ETFs/ ETNs | NaN | - | - | - | - | - | - | 20211230.0 | 2073.5 | 7.917718e+09 | 1.641739e+13 | False |
| 3 | 1308 | 20211230 | Nikko Exchange Traded Index Fund TOPIX | ETFs/ ETNs | NaN | - | - | - | - | - | - | 20211230.0 | 2053.0 | 3.736943e+09 | 7.671945e+12 | False |
| 4 | 1309 | 20211230 | NEXT FUNDS ChinaAMC SSE50 Index Exchange Trade... | ETFs/ ETNs | NaN | - | - | - | - | - | - | 20211230.0 | 44280.0 | 7.263200e+04 | 3.216145e+09 | False |
1 | # 定义为数据进行预处理的函数 |
1 | # 17行业的取值情况 |
IT & SERVICES, OTHERS 1155
- 500
RETAIL TRADE 339
COMMERCIAL & WHOLESALE TRADE 311
CONSTRUCTION & MATERIALS 303
ELECTRIC APPLIANCES & PRECISION INSTRUMENTS 293
RAW MATERIALS & CHEMICALS 288
MACHINERY 228
REAL ESTATE 137
FOODS 136
TRANSPORTATION & LOGISTICS 117
AUTOMOBILES & TRANSPORTATION EQUIPMENT 108
FINANCIALS (EX BANKS) 88
BANKS 81
STEEL & NONFERROUS METALS 77
PHARMACEUTICAL 73
IT & SERVICES, OTHERS 33
ELECTRIC POWER & GAS 25
RETAIL TRADE 20
ENERGY RESOURCES 17
CONSTRUCTION & MATERIALS 13
ELECTRIC APPLIANCES & PRECISION INSTRUMENTS 12
BANKS 12
COMMERCIAL & WHOLESALE TRADE 9
FINANCIALS (EX BANKS) 9
REAL ESTATE 6
RAW MATERIALS & CHEMICALS 6
AUTOMOBILES & TRANSPORTATION EQUIPMENT 6
PHARMACEUTICAL 4
ENERGY RESOURCES 3
STEEL & NONFERROUS METALS 3
MACHINERY 2
TRANSPORTATION & LOGISTICS 2
FOODS 1
Name: 17SectorName, dtype: int64
1 | # 预处理股票数据集 |
1 | # 调整股价 |
1 | # 使用工作日作为窗口 |
/opt/conda/lib/python3.7/site-packages/ipykernel_launcher.py:13: FutureWarning: Series.dt.weekofyear and Series.dt.week have been deprecated. Please use Series.dt.isocalendar().week instead.
del sys.path[0]
关于窗口大小的选择
什么时候该使用哪种窗口大小,这取决于以下因素:
- 目标的时间尺度:如果你关心的是短期的价格动态,那么短的窗口大小可能更有用。反之,如果关心的是长期趋势,那么长的窗口大小可能更合适。
- 数据的频率:对于高频数据(如每分钟或每小时的数据),以天为单位的窗口就会失去意义。例如,对于分钟级的金融数据,可能会选择5分钟、10分钟、30分钟、1小时等作为窗口大小,以捕捉到短期内的价格动态。而对于小时级数据,可能会选择2小时、4小时、12小时等作为窗口大小。
- 模型的复杂性:使用过多的窗口大小可能会导致特征的维度过高,这可能会使模型过拟合。需要根据模型的复杂性和可用数据的量来选择合适的窗口大小。
- 实验与验证:最佳的窗口大小可能需要通过交叉验证或其他模型验证技术来确定。在公司实际的业务中,技术组都会积累自己的技术经验,而作为竞赛,我们需要根据我们对数据业务层面的理解选择窗口,例如,在金融领域,5、10、20、30、50、200天是经常被使用的滚动窗口大小,它们具有很强的传统和经验依据。例如,20天大致对应一个月的交易日(去掉周末和节加日),50天和200天是许多技术分析师用于短期和长期移动平均线的标准。
1 | new_feature.head() |
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
| Date | SecuritiesCode | Open | High | Low | Close | Volume | AdjustmentFactor | SupervisionFlag | Target | ... | ExpMovingAvg_70Day | expand_mean | Upper_Shadow | Lower_Shadow | Date_year | Date_month | Date_day | Date_quarter | Date_weekofyear | Date_dayofweek | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2017-01-04 | 1301 | 2734.0 | 2755.0 | 2730.0 | 2742.0 | 31400 | 1.0 | 0 | 0.000730 | ... | 2742.000000 | 2742.0 | 13.0 | 4.0 | 2017 | 1 | 4 | 1 | 1 | 2 |
| 1861349 | 2017-01-04 | 8185 | 2800.0 | 2847.0 | 2785.0 | 2846.0 | 57500 | 1.0 | 0 | 0.015827 | ... | 2741.887324 | 2740.0 | 1.0 | 15.0 | 2017 | 1 | 4 | 1 | 1 | 2 |
| 285188 | 2017-01-04 | 2698 | 1770.0 | 1783.0 | 1764.0 | 1778.0 | 45700 | 1.0 | 0 | 0.005062 | ... | 2741.834160 | 2740.0 | 5.0 | 6.0 | 2017 | 1 | 4 | 1 | 1 | 2 |
| 1860147 | 2017-01-04 | 8182 | 1482.0 | 1512.0 | 1482.0 | 1510.0 | 50700 | 1.0 | 0 | 0.000649 | ... | 2742.007846 | 2742.0 | 2.0 | 0.0 | 2017 | 1 | 4 | 1 | 1 | 2 |
| 286390 | 2017-01-04 | 2702 | 3090.0 | 3090.0 | 3050.0 | 3060.0 | 384900 | 1.0 | 0 | 0.000000 | ... | 2742.092132 | 2742.6 | 0.0 | 10.0 | 2017 | 1 | 4 | 1 | 1 | 2 |
5 rows × 118 columns
1 | # 增加弱评估器的个数到700,减小学习率为0.01 |
2 行业分段建模 Industry Segmented Modeling
在金融市场中,不同的行业具有独特的特点和动态变化模式(请看下面每个行业每天的平均收盘价的图)。对于投资者和分析师来说,理解并应对这些差异至关重要。行业分段建模是一个专门设计来解决这个问题的方法。
此方法采用一个简单但强大的思路:为每一个行业构建独立的模型,确保每个模型可以专注于学习并预测该行业的特定特点和趋势。这种方法提供了三个主要的优势:
-
专业性: 每个行业的模型都是为了捕捉该行业的独特特点和规律而训练的。这确保了模型不会受到其他行业动态的干扰,提高了预测的准确性。虽然所有行业都受到宏观经济的影响,但影响的程度和方式可能会有所不同。例如,利率上升可能对房地产和金融行业的股票产生不同的影响。单独为每个行业建模可以更好地捕捉行业内部的这些特定动态。
-
灵活性: 由于每个行业都有其独立的模型,当某个行业发生重大变化或面临新的挑战时,可以单独调整或重新训练该行业的模型,而不会影响到其他行业。
-
模型复杂性:当我们为每个行业单独建模时,模型可能不需要太复杂,因为它只需考虑特定行业的特点,而不是整个市场的复杂性。
行业分段建模不仅仅是一个技术方法,更是一种思维方式。它鼓励我们从宏观和微观的角度同时考虑问题,确保我们的策略和决策既有广度又有深度。
值得一提的是,这种策略并不是只适用于股票数据集,这种思维还可以应用到别的行业当中:
-
气候建模:气候模型可能会独立地为不同的地理区域或气候带进行建模,因为每个区域的气候驱动因素可能会有所不同。
-
零售与销售预测:大型零售商可能会为不同的产品类别、地区或季节分别进行销售预测。
-
网站流量预测:对于大型网站,流量预测可能会基于网站的不同板块(新闻、论坛、博客、产品页)或不同用户群体进行。
-
医疗研究:在研究某种疾病或治疗方法时,可能会单独考虑不同的人群、年龄组或性别。例如,某些疾病仅在男性或女性中更为常见;同样,某些药物或治疗对于男性和女性的效果可能不同。
1 | # 本地运行代码 |
1 | train_df = train_df.merge(stock_list_df[['SecuritiesCode','17SectorName']],on='SecuritiesCode') |
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
| RowId | Date | SecuritiesCode | Open | High | Low | Close | Volume | AdjustmentFactor | ExpectedDividend | SupervisionFlag | Target | 17SectorName | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 20170104_1301 | 2017-01-04 | 1301 | 2734.0 | 2755.0 | 2730.0 | 2742.0 | 31400 | 1.0 | NaN | False | 0.000730 | FOODS |
| 1 | 20170105_1301 | 2017-01-05 | 1301 | 2743.0 | 2747.0 | 2735.0 | 2738.0 | 17900 | 1.0 | NaN | False | 0.002920 | FOODS |
| 2 | 20170106_1301 | 2017-01-06 | 1301 | 2734.0 | 2744.0 | 2720.0 | 2740.0 | 19900 | 1.0 | NaN | False | -0.001092 | FOODS |
| 3 | 20170110_1301 | 2017-01-10 | 1301 | 2745.0 | 2754.0 | 2735.0 | 2748.0 | 24200 | 1.0 | NaN | False | -0.005100 | FOODS |
| 4 | 20170111_1301 | 2017-01-11 | 1301 | 2748.0 | 2752.0 | 2737.0 | 2745.0 | 9300 | 1.0 | NaN | False | -0.003295 | FOODS |
1 | # 按照行业分组并计算每个行业每天的平均收盘价 |
1 | # 从文件中读取数据 |
1 | # 查看股票列表数据集 |
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
| SecuritiesCode | EffectiveDate | Name | Section/Products | NewMarketSegment | 33SectorCode | 33SectorName | 17SectorCode | 17SectorName | NewIndexSeriesSizeCode | NewIndexSeriesSize | TradeDate | Close | IssuedShares | MarketCapitalization | Universe0 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1301 | 20211230 | KYOKUYO CO.,LTD. | First Section (Domestic) | Prime Market | 50 | Fishery, Agriculture and Forestry | 1 | FOODS | 7 | TOPIX Small 2 | 20211230.0 | 3080.0 | 1.092828e+07 | 3.365911e+10 | True |
| 1 | 1305 | 20211230 | Daiwa ETF-TOPIX | ETFs/ ETNs | NaN | - | - | - | - | - | - | 20211230.0 | 2097.0 | 3.634636e+09 | 7.621831e+12 | False |
| 2 | 1306 | 20211230 | NEXT FUNDS TOPIX Exchange Traded Fund | ETFs/ ETNs | NaN | - | - | - | - | - | - | 20211230.0 | 2073.5 | 7.917718e+09 | 1.641739e+13 | False |
| 3 | 1308 | 20211230 | Nikko Exchange Traded Index Fund TOPIX | ETFs/ ETNs | NaN | - | - | - | - | - | - | 20211230.0 | 2053.0 | 3.736943e+09 | 7.671945e+12 | False |
| 4 | 1309 | 20211230 | NEXT FUNDS ChinaAMC SSE50 Index Exchange Trade... | ETFs/ ETNs | NaN | - | - | - | - | - | - | 20211230.0 | 44280.0 | 7.263200e+04 | 3.216145e+09 | False |
1 | # 选择股票列表中属于"Universe0"(股票池)的股票 |
1 | sec_info.head() |
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
| SecuritiesCode | 33SectorName | 17SectorName | |
|---|---|---|---|
| 0 | 1301 | Fishery, Agriculture and Forestry | FOODS |
| 19 | 1332 | Fishery, Agriculture and Forestry | FOODS |
| 20 | 1333 | Fishery, Agriculture and Forestry | FOODS |
| 37 | 1375 | Fishery, Agriculture and Forestry | FOODS |
| 38 | 1376 | Fishery, Agriculture and Forestry | FOODS |
1 | # 和时序特征工程类似,但是需要将股票信息的数据并入股价数据集 |
update介绍
update 函数用于根据另一个 DataFrame 来更新原始 DataFrame 中的数据。它会根据提供的 DataFrame 中的索引和列名来更新原始 DataFrame 中对应的数据。
1 | # 创建原始 DataFrame |
A B C
0 1 40 70
1 2 50 80
2 3 60 90
1 | # 对数据进行预处理 |
模型融合的目标是通过结合多个模型来捕捉它们的优势和补偿它们的不足,从而提高整体的预测性能。融合多个相似的模型可能不会带来太大的性能提升,因为它们可能会重复相同的错误。为了最大化模型融合的潜力,模型之间应该存在一定的差异性。有几种策略可以增加模型的差异性:
-
模型结构的差异性:选择原理或结构差异大的模型进行融合,可以帮助捕捉数据中的不同信息。例如,LightGBM 是基于树的模型,它能够捕捉非线性关系。而线性回归则关注数据的线性关系。结合这两者,可以提高模型的泛化性能。
-
特征工程的差异性:即使我们使用相同的模型,如多个 LightGBM 实例,通过为它们提供不同的特征集也可以引入差异性。这种方法的思想是,不同的特征集可能会揭示数据的不同方面,从而使模型对数据有不同的理解。
-
超参数的差异性:对于同一模型,使用不同的超参数设置也会产生不同的模型。例如,不同的树深度、学习率或正则化项都可以为模型引入差异。
基于上面的思路,我们可以在第二套特征工程上尝试对数据进一步进行探索和尝试:
对于时序数据,并不总是需要使用尽可能长时间的数据。如果目标是捕捉长期趋势,那么使用更长的时间段会更有意义。如果我们更关心的是短期的变化或波动,那么最近的数据可能更为相关(当前目标也就是预测未来三个月某一天的收益率)。因此,我们可以尝试选取不同时长的时序数据,以时间作为一个切入点。
1 | # 并且我们在探索的过程中发现核心股价数据集的日期有一些特点 |
2017-01-04 1865
2017-01-05 1865
2017-01-06 1865
2017-01-10 1865
2017-01-11 1865
...
2021-11-29 2000
2021-11-30 2000
2021-12-01 2000
2021-12-02 2000
2021-12-03 2000
Name: Date, Length: 1202, dtype: int64
思考: 日期的取值情况是什么意思?为什么17年的时候只有1865?这1865是什么含义?
对数字敏感的小伙伴可能会立马注意到,2000就是股票池里的股票的个数。按道理来说,每天都应该有2000支股票的数据才对,为什么会出现小于2000的情况? —— 回顾jpx构建股票池的规则:市值排名前2000的股票,而且这些股票必须在交易所上市至少一年。
可是这个“一年”到底是什么时候开始计算的呢?可以看到核心股价数据集的最新日期为2021-12-03,如果往前推一年的话应该是2020-12-03。也就是说,2020-12-03开始,股票池中2000支股票的数据就应该都被记录在核心股价数据集中。下面我们来验证我们的想法:
1 | # 选取2020-12-03之后的某一天,我们发现虽然已经很接近2000了,但是还是差一些 |
1996
我们换一种思路:
1 | # 计算Date字段取值出现次数 |
.dataframe thead th {
text-align: right;
}
| RowId | Date | SecuritiesCode | Open | High | Low | Close | Volume | AdjustmentFactor | ExpectedDividend | SupervisionFlag | Target | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 20170104_1301 | 2017-01-04 | 1301 | 2734.0 | 2755.0 | 2730.0 | 2742.0 | 31400 | 1.0 | NaN | False | 0.000730 |
| 1 | 20170104_1332 | 2017-01-04 | 1332 | 568.0 | 576.0 | 563.0 | 571.0 | 2798500 | 1.0 | NaN | False | 0.012324 |
| 2 | 20170104_1333 | 2017-01-04 | 1333 | 3150.0 | 3210.0 | 3140.0 | 3210.0 | 270800 | 1.0 | NaN | False | 0.006154 |
| 3 | 20170104_1376 | 2017-01-04 | 1376 | 1510.0 | 1550.0 | 1510.0 | 1550.0 | 11300 | 1.0 | NaN | False | 0.011053 |
| 4 | 20170104_1377 | 2017-01-04 | 1377 | 3270.0 | 3350.0 | 3270.0 | 3330.0 | 150800 | 1.0 | NaN | False | 0.003026 |
1 | # 查看后五行 |
.dataframe thead th {
text-align: right;
}
| RowId | Date | SecuritiesCode | Open | High | Low | Close | Volume | AdjustmentFactor | ExpectedDividend | SupervisionFlag | Target | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1868526 | 20201222_9990 | 2020-12-22 | 9990 | 533.0 | 533.0 | 524.0 | 525.0 | 97900 | 1.0 | NaN | False | 0.009634 |
| 1868527 | 20201222_9991 | 2020-12-22 | 9991 | 976.0 | 976.0 | 951.0 | 960.0 | 19100 | 1.0 | NaN | False | 0.010460 |
| 1868528 | 20201222_9993 | 2020-12-22 | 9993 | 1778.0 | 1782.0 | 1763.0 | 1777.0 | 2000 | 1.0 | NaN | False | -0.001674 |
| 1868529 | 20201222_9994 | 2020-12-22 | 9994 | 2167.0 | 2170.0 | 2153.0 | 2165.0 | 5300 | 1.0 | NaN | False | 0.001856 |
| 1868530 | 20201222_9997 | 2020-12-22 | 9997 | 1013.0 | 1017.0 | 981.0 | 981.0 | 266400 | 1.0 | NaN | False | 0.045918 |
1 | # 查看不到2000支股票的最后一个交易日 |
'2020-12-22'
1 | # 查看不到2000支股票的最早的交易日 |
'2017-01-04'
也就是说,2020-12-23以后的数据都会包含股票池里完整的2000支股票(猜对了年月,没有猜对具体日子),在此之前有些股票的数据都是缺失的,所以我们尝试选取包含完整股票池所有股票信息的数据进行建模。
1 | train_df[train_df.Date=='2020-12-23'].SecuritiesCode.nunique() |
2000
1 | # 所以我们筛选有2000支股票数据 |
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
| Date | SecuritiesCode | Open | High | Low | Close | Volume | AdjustmentFactor | ExpectedDividend | SupervisionFlag | Target | 33SectorName | 17SectorName | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 970 | 2020-12-23 | 1301 | 2913.0 | 2920.0 | 2906.0 | 2913.0 | 6300 | 1.0 | 0.0 | 0 | -0.000343 | Fishery, Agriculture and Forestry | FOODS |
| 971 | 2020-12-24 | 1301 | 2913.0 | 2937.0 | 2909.0 | 2917.0 | 13900 | 1.0 | 0.0 | 0 | 0.011660 | Fishery, Agriculture and Forestry | FOODS |
| 972 | 2020-12-25 | 1301 | 2903.0 | 2930.0 | 2903.0 | 2916.0 | 8300 | 1.0 | 0.0 | 0 | 0.003729 | Fishery, Agriculture and Forestry | FOODS |
| 973 | 2020-12-28 | 1301 | 2929.0 | 2950.0 | 2910.0 | 2950.0 | 22900 | 1.0 | 0.0 | 0 | -0.003377 | Fishery, Agriculture and Forestry | FOODS |
| 974 | 2020-12-29 | 1301 | 2948.0 | 2963.0 | 2945.0 | 2961.0 | 15100 | 1.0 | 0.0 | 0 | -0.000339 | Fishery, Agriculture and Forestry | FOODS |
为了实现这个策略,我们选择了LightGBM作为基础模型,因为它在很多实际应用中都表现出色。但是,标准的LightGBM库并不直接支持行业分段建模。因此,我们决定设计一个自定义类,使得行业分段建模变得简单和直观。
这个自定义类的目标是:
- 提供与LightGBM类似的接口,以便于使用。例如,我们可以使用model.train进行训练,使用model.predict进行预测。
- 在调用这些方法时,自动实现行业分段建模。也就是说,当我们调用model.train时,该类会自动为每个行业训练一个独立的模型;当我们调用model.predict时,它会使用相应的模型为每个行业进行预测。
下面是实现代码:
1 | # 使用LightGBM模型进行训练和预测 |
predict部分详解
1 | # 创建一个示例数据集 |
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
| 33SectorName | SecuritiesCode | feature | |
|---|---|---|---|
| 0 | Services | 1301 | 5 |
| 1 | Services | 1302 | 6 |
| 2 | Foods | 1303 | 7 |
| 3 | Foods | 1304 | 8 |
1 | # 如果我们直接对训练集按照行业分组的话,返回的对象是一个DataFrameGroupBy 对象 |
<pandas.core.groupby.generic.DataFrameGroupBy object at 0x000001E731047188>
1 | # 既然是可迭代对象,我们就可以使用for循环取出 |
Group: Foods
33SectorName SecuritiesCode feature
2 Foods 1303 7
3 Foods 1304 8
Group: Services
33SectorName SecuritiesCode feature
0 Services 1301 5
1 Services 1302 6
1 | y_pred = np.array([ 0.00113337, -0.01450866, 0.01365594, 0.00297457]) |
1 | sec_codes = group['SecuritiesCode'] |
0 1301
1 1302
Name: SecuritiesCode, dtype: int64
1 | # 接受两个或多个可迭代对象作为参数,并返回一个迭代器 |
<zip at 0x1e731121948>
1 | list(zip(sec_codes, y_pred)) |
[(1301, 0.00113337), (1302, -0.01450866)]
1 | y_preds = [] |
[(1301, 0.00113337), (1302, -0.01450866)]
1 | pd.DataFrame(y_preds, columns=["codes", "pred"]) |
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
| codes | pred | |
|---|---|---|
| 0 | 1301 | 0.001133 |
| 1 | 1302 | -0.014509 |
1 | # 实例化模型并训练 |
/opt/conda/lib/python3.7/site-packages/lightgbm/sklearn.py:736: UserWarning: 'verbose' argument is deprecated and will be removed in a future release of LightGBM. Pass 'log_evaluation()' callback via 'callbacks' argument instead.
_log_warning("'verbose' argument is deprecated and will be removed in a future release of LightGBM. "
如果我不想使用lightgbm模型进行训练怎么办?
1 | # 使用LinearRegression模型进行训练和预测 |
1 | # train_df |
1 | # 实例化模型并训练 |
三、模型融合与提交
加入线性模型抗过拟合
1 | # 定义为训练数据进行特征工程的函数 |
1 | # 加载数据集 |
1 | # 对数据进行特征工程 |
1 | # Instantiation and Learning |
LinearRegression()
导入测试模块
1 | env = jpx_tokyo_market_prediction.make_env() |
1 | cols=['Date','SecuritiesCode','Open','High','Low','Close','Volume','AdjustmentFactor','SupervisionFlag','17SectorName'] |

