logo

时间序列特征工程

王哲峰 / 2022-09-13


目录

时间序列特征

img

时间序列特征构造基本准则:

动态特征

离散时间特征

基础周期特征:

特殊周期特征:

连续时间特征

时间差

静态特征

静态特征即随着时间的变化,不会发生变化的信息

除了最细粒度的唯一键,还可以加入其它形式的静态特征

类别型特征

另外一类最常见的基础特征,就是区分不同序列的类别特征, 例如不同的门店,商品,或者不同的股票代码等

通过加入这个类别特征,就可以把不同的时间序列数据放在一张大表中统一训练了。 模型理论上来说可以自动学习到这些类别之间的相似性,提升泛化能力

数值型特征

动态特征

Lag 特征、日期时间衍生特征这类属于动态特征。随着时间变化会发生改变。这其中又可以分成两类:

将时间序列在时间轴上划分窗口是一个常用且有效的方法, 包括滑动窗口(根据指定的单位长度来框住时间序列,每次滑动一个单位), 与滚动窗口(根据指定的单位长度来框住时间序列,每次滑动窗口长度的多个单位)

窗口分析对平滑噪声或粗糙的数据非常有用,比如移动平均法等,这种方式结合基础的统计方法, 即按照时间的顺序对每一个时间段的数据进行统计,从而可以得到每个时间段内目标所体现的特征, 进而从连续的时间片段中,通过对同一特征在不同时间维度下的分析,得到数据整体的变化趋势

基础周期特征

几乎所有的日期时间都可以被拆解为:年-月-日-小时-分钟-秒-毫秒 的形式。 虽然拆解很简单,但是里面会含有非常多的潜在重要信息,如果直接对时间信息进行 Label 编码, 然后使用梯度提升树模型进行训练预测,是极难挖掘到此类信息的, 但是拆解之后却可以极大的帮助到梯度提升树模型发现此类信息

在大多数情况中,拆解之后的数据往往存在某些潜在规律的,比如:

以下是可以生成的完整功能列表:

img

日期特征

import pandas as pd

data = pd.read_csv('Train_SU63ISt.csv')
data['Datetime'] = pd.to_datetime(data['Datetime'],format='%d-%m-%Y %H:%M')

data['year']=data['Datetime'].dt.year 
data['month']=data['Datetime'].dt.month 
data['day']=data['Datetime'].dt.day

data['dayofweek_num']=data['Datetime'].dt.dayofweek  
data['dayofweek_name']=data['Datetime'].dt.weekday_name

data.head()

时间特征

import pandas as pd

data = pd.read_csv('Train_SU63ISt.csv')
data['Datetime'] = pd.to_datetime(data['Datetime'],format='%d-%m-%Y %H:%M')

data['Hour'] = data['Datetime'].dt.hour 
data['minute'] = data['Datetime'].dt.minute 

data.head()

特殊周期特征

特殊周期特征指比如对月、星期、节假日等的特征:

月份特征

该特征经常适用于关于以月为单位的预估问题,例如预估某个公司每个月的产值,某个景点的旅游人数, 这个时候每个月中工作日的天数以及休假的天数就是非常重要的信息。比如:

img

def month_features(timeindex, timeformat: str):
    n = len(timeindex)
    features = np.zeros((n, 1))
    features[:, 0] = timeindex.month.values / 12
    return features

星期特征

def week_features(timeindex, timeformat: str):
    n = len(timeindex)
    features = np.zeros((n, 1))
    features[:, 0] = timeindex.weekday.values / 6
    return features

节假日特征

def holidays_features(timeindex, timeformat: str, holidays: tuple):
    n = len(timeindex)
    features = np.zeros((n, 1))
    for i in range(n):
        if timeindex[i].strftime(timeformat) in holidays:
            features[i, 0] = 1
    return features

holidays = (
    '20130813', '20130902', '20131001', '20131111', 
    '20130919', '20131225', '20140101', '20140130', 
    '20140131', '20140214', '20140405', '20140501', 
    '20140602', '20140802', '20140901', '20140908'
)

组合特征

连续时间特征

相邻时间差

该特征顾明思议,就是相邻两个时间戳之间的差值,在有些问题中, 例如用户浏览每个视频的开始时间戳,相邻两个时间戳的差值一般就是用户浏览视频的差值, 如果差值越大,那么该用户可能对上一个视频的喜好程度往往越大,此时,相邻时间戳的差值就是非常有价值的特征

该特征往往适合相邻时间差互补的一个特征,可以帮助更好地挖掘一些内在的信息, 例如有些自律的用户在会控制自己的休闲与工作的时长,在统计用户的生活习惯时, 发现出现大量的相邻时间差时 10 分钟和 60 分钟的,原来是该用户喜欢工作 60 分钟就休息 10 分钟, 此时相邻时间差频率编码就可以协助发现此类信息

持续时间

间隔时间

滞后特征

Lag Features

为了便于理解,可以假设预测的 horizon 长度仅为 1 天,而历史的特征 window 长度为 7 天, 那么可以构建的最基础的特征即为过去 7 天的每天的历史值,来预测第 8 天的值。 这个历史 7 天的值,在机器学习类方法中,一般被称为 lag 特征

创建滞后特征

import pandas as pd 

def series_to_supervised(data, n_lag = 1, n_fut = 1, selLag = None, selFut = None, dropnan = True):
    """
    Converts a time series to a supervised learning data set by adding time-shifted prior and future period
    data as input or output (i.e., target result) columns for each period
    :param data:  a series of periodic attributes as a list or NumPy array
    :param n_lag: number of PRIOR periods to lag as input (X); generates: Xa(t-1), Xa(t-2); min= 0 --> nothing lagged
    :param n_fut: number of FUTURE periods to add as target output (y); generates Yout(t+1); min= 0 --> no future periods
    :param selLag:  only copy these specific PRIOR period attributes; default= None; EX: ['Xa', 'Xb' ]
    :param selFut:  only copy these specific FUTURE period attributes; default= None; EX: ['rslt', 'xx']
    :param dropnan: True= drop rows with NaN values; default= True
    :return: a Pandas DataFrame of time series data organized for supervised learning
    
    NOTES:
    (1) The current period's data is always included in the output.
    (2) A suffix is added to the original column names to indicate a relative time reference: e.g., (t) is the current
        period; (t-2) is from two periods in the past; (t+1) is from the next period
    (3) This is an extension of Jason Brownlee's series_to_supervised() function, customized for MFI use
    """
    n_vars = 1 if type(data) is list else data.shape[1]
    df = pd.DataFrame(data)
    origNames = df.columns
    cols, names = list(), list()
    # include all current period attributes
    cols.append(df.shift(0))
    names += [("%s" % origNames[j]) for j in range(n_vars)]
    # lag any past period attributes (t-n_lag, ..., t-1)
    n_lag = max(0, n_lag) # force valid number of lag periods
    # input sequence (t-n, ..., t-1)
    for i in range(n_lag, 0, -1):
        suffix = "(t-%d)" % i
        if (None == selLag):
        cols.append(df.shift(i))
        names += [("%s%s" % (origNames[j], suffix)) for j in range(n_vars)]
        else:
        for var in (selLag):
                cols.append(df[var].shift(i))
                names += [("%s%s" % (var, suffix))]
    # include future period attributes (t+1, ..., t+n_fut)
    n_fut = max(n_fut, 0)
    # forecast sequence (t, t+1, ..., t+n)
    for i in range(0, n_fut + 1):
        suffix = "(t+%d)" % i
        if (None == selFut):
        cols.append(df.shift(-i))
        names += [("%s%s" % (origNames[j], suffix)) for j in range(n_vars)]
        else:
        for var in (selFut):
                cols.append(df[var].shift(-i))
                names += [("%s%s" % (var, suffix))]
    # put it all together
    agg = pd.concat(cols, axis = 1)
    agg.columns = names
    # drop rows with NaN values
    if dropnan:
        agg.dropna(inplace = True)

    return agg

示例:

import pandas as pd
data = pd.read_csv('Train_SU63ISt.csv')
data['Datetime'] = pd.to_datetime(data['Datetime'],format='%d-%m-%Y %H:%M')

data['lag_1'] = data['Count'].shift(1)
data['lag_2'] = data['Count'].shift(2)
data['lag_3'] = data['Count'].shift(3)
data['lag_4'] = data['Count'].shift(4)
data['lag_5'] = data['Count'].shift(5)
data['lag_6'] = data['Count'].shift(6)
data['lag_7'] = data['Count'].shift(7)

data = data[['Datetime', 'lag_1', 'lag_2', 'lag_3', 'lag_4', 'lag_5', 'lag_6', 'lag_7', 'Count']]
data.head(10)

窗口特征

Lag 的基本属于直接输入的信息,基于这些信息,我们还可以进一步做各种复杂的衍生特征。 例如在 lag 的基础上,我们可以做各种窗口内的统计特征,比如过去 n 个时间点的平均值,最大值,最小值,标准差等。 进一步,还可以跟之前的各种维度信息结合起来来计算,比如某类商品的历史均值,某类门店的历史均值等。 也可以根据自己的理解,做更复杂计算的衍生,例如过去 7 天中,销量连续上涨的天数, 过去 7 天中最大销量与最低销量之差等等

滚动窗口特征

如何根据过去的值计算一些统计值?这种方法称为滚动窗口方法,因为每个数据点的窗口都不同

由于这看起来像一个随着每个下一个点滑动的窗口,因此使用此方法生成的特征称为 “滚动窗口” 特征。 我们将选择一个窗口大小,取窗口中值的平均值,并将其用作特征。让我们在 Python 中实现它

示例 1:

import pandas as pd
data = pd.read_csv('Train_SU63ISt.csv')
data['Datetime'] = pd.to_datetime(data['Datetime'],format='%d-%m-%Y %H:%M')

data['rolling_mean'] = data['Count'].rolling(window=7).mean()
data = data[['Datetime', 'rolling_mean', 'Count']]
data.head(10)

示例 2:

temps = pd.DataFrame(series.values)

shifted = temps.shift(1)
window = shifted.rolling(window = 2)
means = window.mean()

df = pd.concat([mean, temps], axis = 1)
df.columns = ["mean(t-2,t-1)", "t+1"]

print(df.head())
   mean(t-2,t-1)   t+1
0            NaN  17.9
1            NaN  18.8
2          18.35  14.6
3          16.70  15.8
4          15.20  15.8

示例 3:

temps = pd.DataFrame(series.values)

width = 3
shifted = temps.shift(width - 1)
window = shifted.rolling(windon = width)

df = pd.concat([
    window.min(), 
    window.mean(), 
    window.max(), 
    temps
], axis = 1)
df.columns = ["min", "mean", "max", "t+1"]

print(df.head())
    min  mean   max   t+1
0   NaN   NaN   NaN  17.9
1   NaN   NaN   NaN  18.8
2   NaN   NaN   NaN  14.6
3   NaN   NaN   NaN  15.8
4  14.6  17.1  18.8  15.8

扩展窗口特征

在滚动窗口的情况下,窗口的大小是恒定的,扩展窗口功能背后的想法是它考虑了所有过去的值。 每一步,窗口的大小都会增加 1,因为它考虑了系列中的每个新值

示例 1:

import pandas as pd
data = pd.read_csv('Train_SU63ISt.csv')
data['Datetime'] = pd.to_datetime(data['Datetime'],format='%d-%m-%Y %H:%M')

data['expanding_mean'] = data['Count'].expanding(2).mean()
data = data[['Datetime','Count', 'expanding_mean']]
data.head(10)

实例 2:

temps = pd.DataFrame(series.values)

window = temps.expanding()

df = pd.concat([
    window.min(), 
    window.mean(), 
    window.max(), 
    temps.shift(-1)
], axis = 1)
df.columns = ["min", "mean", "max", "t+1"]

print(df.head())
    min    mean   max   t+1
0  17.9  17.900  17.9  18.8
1  17.9  18.350  18.8  14.6
2  14.6  17.100  18.8  15.8
3  14.6  16.775  18.8  15.8
4  14.6  16.580  18.8  15.8

历史统计特征

统计特征

对时间序列进行统计分析是最容易想到的特征提取方法。基于历史数据构造长中短期的统计值, 包括前 n 天/周期内的

峰值特征

序列中峰值的个数,序列中峰值的个数可以间接反映序列的波动情况, 这在非常多的问题中都有着非常强的物理意义,例如:

可以使用scipy.signal 进行峰值个数特征的构建,捕捉到以下这些峰值的情况:

峰值的个数:

import matplotlib.pyplot as plt
from scipy.misc import electrocardiogram
from scipy.signal import find_peaks
import numpy as np

x = electrocardiogram()[2000:4000]
peaks, _ = find_peaks(x, height = 0)
plt.plot(x)
plt.plot(peaks, x[peaks], "x")
plt.plot(np.zeros_like(x), "--", color = "gray")
plt.show()

较大距离的峰值:

peaks, _ = find_peaks(x, distance = 150)
np.diff(peaks)

plt.plot(x)
plt.plot(peaks, x[peaks], "x")
plt.show()

Python API

from pandas.plotting import autocorrelation_plot

# 自相关性系数图
autocorrelation_plot(data['value'])

# 构造过去 n 天的统计数据
def get_statis_n_days_num(data, col, n):
    temp = pd.DataFrame()
    for i in range(n):
        temp = pd.concat([temp, data[col].shift((i + 1) * 24)], axis = 1)
        data['avg_' + str(n) + '_days_' + col] = temp.mean(axis = 1)
        data['median_' + str(n) + '_days_' + col] = temp.median(axis = 1)
        data['max_' + str(n) + '_days_' + col] = temp.max(axis = 1)
        data['min_' + str(n) + '_days_' + col] = temp.min(axis = 1)
        data['std_' + str(n) + '_days_' + col] = temp.std(axis = 1)
        data['mad_' + str(n) + '_days_' + col] = temp.mad(axis = 1)
        data['skew_' + str(n) + '_days_' + col] = temp.skew(axis = 1)
        data['kurt_' + str(n) + '_days_' + col] = temp.kurt(axis = 1)
        data['q1_' + str(n) + '_days_' + col] = temp.quantile(q = 0.25, axis = 1)
        data['q3_' + str(n) + '_days_' + col] = temp.quantile(q = 0.75, axis = 1)
        data['var_' + str(n) + '_days_' + col] = data['std' + str(n) + '_days_' + col] / data['avg_' + str(n) + '_days_' + col]

    return data

data_df = get_statis_n_days_num(data_df, 'num_events', n = 7)
data_df = get_statis_n_days_num(data_df, 'num_events', n = 14)
data_df = get_statis_n_days_num(data_df, 'num_events', n = 21)
data_df = get_statis_n_days_num(data_df, 'num_events', n = 28)
# n个星期前的同期特征
data_df['ago_7_day_num_events'] = data_df['num_events'].shift(7 * 24)
data_df['ago_14_day_num_events'] = data_df['num_events'].shift(14 * 24)
data_df['ago_21_day_num_events'] = data_df['num_events'].shift(21 * 24)
data_df['ago_28_day_num_events'] = data_df['num_events'].shift(28 * 24)

# 昨天的同期特征
data_df['ago_7_day_num_events'] = data_df['num_events'].shift(1 * 24)

滞后窗口统计特征

交叉特征

特征交叉一般从重要特征下手, 慢工出细活

类别特征与类别特征

类别特征间组合构成新特征

连续特征与类别特征

连续特征与连续特征

Python API

# 一阶差分
data_df['ago_28_21_day_num_trend'] = data_df['ago_28_day_num_events'] - data_df['ago_21_day_num_events']
data_df['ago_21_14_day_num_trend'] = data_df['ago_21_day_num_events'] - data_df['ago_14_day_num_events']
data_df['ago_14_7_day_num_trend'] = data_df['ago_14_day_num_events'] - data_df['ago_7_day_num_events']
data_df['ago_7_1_day_num_trend'] = data_df['ago_7_day_num_events'] - data_df['ago_1_day_num_events']

隐蔽特征

在时间序列问题中,经常习惯做非常多的手工统计特征,包括一些序列的近期的情况、 序列的趋势信息、周期信息等等。除了手动构建特征之外,其实还存在许多非常隐蔽的特征, 这些特征是直接通过模型产出的,这里介绍基于线性模型的特征

基于线性模型的特征

线性模型的系数特征

通过线性回归的系数来表示近期的趋势特征

from sklearn import linear_model

reg = linear_model.LinearRegression()
reg.fit(X, y)
reg.coef_

线性模型的残差特征

直接计算线性回归的预测结果与真实值差值(残差), 并计算所有差值的均值作为残差特征

import numpy as np
from sklearn import linear_model

reg = linear_model.LinearRegression()
reg.fit(X, y)
preds = reg.predict(X)
residual = np.mean(np.abs(y - preds))

基于其他模型的特征

元特征

在时间序列等相关的问题中,除了许多传统的时间序列相关的统计特征之外, 还有一类非常重要的特征,这类特征并不是基于手工挖掘的,而是由机器学习模型产出的, 但更为重要的是,它往往能为模型带来巨大的提升

对时间序列抽取元特征,一共需要进行两个步骤:

元特征抽取

元特征抽取部分,操作如下:

img

元特征预测

将元特征作为新的特征,与原来的数据进行拼接,重新训练新的模型, 并用新训练的模型预测得到最终的结果

img

转换特征

对时序数据进行分析的时候,常常会发现数据中存在一些问题, 使得不能满足一些分析方法的要求(比如:正态分布、平稳性等), 其常常需要我们使用一些变换方法对数据进行转换; 另一方面,人工的特征分析方法局限于人的观察经验, 许多高维且隐秘的特征单单靠人力难以发现。 因此,许多工作尝试对时序数据进行转换,从而捕捉更多的特征

统计转换特征

1964 年提出的 Box-Cox 变换可以使得线性回归模型满足线性、独立性、方差齐次性和正态性的同时又不丢失信息, 其变换的目标有两个:

高维空间转换特征

高维空间转换特征直白点说就是把一维的时序转化到高维。这个高维可能是二维(例如图片), 或者更高维(例如相空间重构)。这种转换可以使得时序的信息被放大,从而暴露更多的隐藏信息。 同时,这种方法增加了数据分析的计算量,一般不适用于大规模的时序分析

格拉姆角场

格拉姆角场,GAF

该转化在笛卡尔坐标系下,将一维时间序列转化为极坐标系表示,再使用三角函数生成 GAF 矩阵。 计算过程:

img

马尔科夫随机场

马尔科夫随机场,MRF

MRF 的基本思想是将时间序列的值状态化,然后计算时序的转化概率, 其构建的是一个概率图(Graph),一种无向图的生成模型,主要用于定义概率分布函数

这里用到了时序窗口分析方法先构建随机场。随机场是由若干个位置组成的整体, 当给每一个位置中按照某种分布随机赋予一个值之后,其全体就叫做随机场

举个例子,假如时序划分片段,所有的片段聚成若干的状态,将时序映射回这些状态上, 我们便得到了一个随机场。有关这个例子可以参考文章《AAAI 2020 | 时序转化为图用于可解释可推理的异常检测》

img

马尔科夫随机场是随机场的特例,它假设随机场中某一个位置的赋值仅仅与和它相邻的位置的赋值有关, 与其不相邻的位置的赋值无关。例如时序片段与有关,与没有关系。

构建马尔科夫随机场,可以更清晰的展现时序分布的转化过程,捕捉更精确的分布变化信息

时频分析

时频分析是一类标准方法,常用在通信领域信号分析中,包括傅里叶变换, 短时傅里叶变换,小波变换等,逐步拟合更泛化的时间序列

img

傅里叶变换是一种线性的积分变换,常在将信号在时域(或空域)和频域之间变换时使用。 其主要处理平稳的时间序列。当时序数据非平稳时,一般的傅里叶变换便不再适用, 这里便有了短时傅里叶变换方法,其主要通过窗口分析, 把整个时域过程分解成无数个等长的小过程,每个小过程近似平稳,再傅里叶变换, 就知道在哪个时间点上出现了什么频率。然而,我们无法保证所有等长的窗口都是平稳的, 手动调整窗口的宽窄成本大,耗费人力。小波分解尝试解决这个问题, 其直接把傅里叶变换的基换了——将无限长的三角函数基换成了有限长的会衰减的小波基。 这样不仅能够获取频率,还可以定位到时间

降维转化特征

与高维空间转换特征相反,提取时间序列的降维特征常出现在多维时间序列分析方面, 其主要是更快捕捉复杂时间序列中的主要特征,提高分析效率与速度, 包括主成分分析(PCA),tSNE,张量分解等等,可以帮助我们从相关因素的角度来理解时间序列

主成分分析是一种分析、简化数据集的技术。其通过保留低阶主成分,忽略高阶主成分做到的。 这样低阶成分往往能够保留住数据的最重要方面。但这也不是一定的,要视具体应用而定。更多可以参考《主成分分析》

张量分解从本质上来说是矩阵分解的高阶泛化,常出现在推荐系统中。在实际应用中, 特征张量往往是一个稀疏矩阵,即很多位置上的元素是空缺的,或者说根本不存在。 举个例子,如果有10000个用户,同时存在10000部电影,我们以此构造一个用户评分行为序列的张量, 这里不经想问:难道每个用户都要把每部电影都看一遍才知道用户的偏好吗? 其实不是,我们只需要知道每个用户仅有的一些评分就可以利用矩阵分解来估计用户的偏好, 并最终推荐用户可能喜欢的电影

img

基于神经网络的特征工程

还有一种转换特征便是通过神经网络的方式自抽取特征表达。 这种方式通常特征的解释性差,但效果好。一般来说,训练好的网络中间层输出可以被当做特征, 例如自编码器模型 “Encoder-Decoder”,如果输入输出是时间序列的话, Encoder 的输出可以当做一个输入被“压缩”的向量,那么当网络效果得还不错的时候, 可以简单看做这个向量具备了这个时序的特征

img

分类特征

分类特征一般结合具体的任务,比如时序预测,时序分类等,常常有标签(Label)信息来引导, 其分析的特征也为具体的任务所服务,是一类常用的特征分析方法, 一般通过机器学习中的有监督方式进行抽取

字典特征

BoP

字典方法旨在将时间序列通过变换,找到划分的阈值,进而将每个时序实值划分开, 对应到某个字母表中。其通过滑动窗提取不同“单词”的出现频率,作为分类依据。 这种方法的优势在于速度很快,而且抗噪效果好,缺点在于会损失很多有效的时序信息, 只能进行粗粒度的时序分类分析

img

形态特征(Shapelet)

形态方法旨在捕捉时间序列分类任务中作为分类依据的有代表性的子序列形状。 2012 年提出的 Shapelet 方法就是搜索这些候选的子序列形状以找到分类的依据, 因为在真实世界中的时间序列往往存在有特征明显的形状, 例如心电图数据一次正常心跳简化一下就是前后两个小的峰中间加一个高峰, 那么如果其中缺了一块形状的话,可能就是作为鉴别异常心跳的依据

img

参考