特徵工程系列:時間特徵構造以及時間序列特徵構造
- 2019 年 11 月 11 日
- 筆記
0x00 前言
數據和特徵決定了機器學習的上限,而模型和演算法只是逼近這個上限而已。由此可見,特徵工程在機器學習中佔有相當重要的地位。在實際應用當中,可以說特徵工程是機器學習成功的關鍵。
那特徵工程是什麼?
特徵工程是利用數據領域的相關知識來創建能夠使機器學習演算法達到最佳性能的特徵的過程。
特徵工程又包含了 Data PreProcessing(數據預處理)、Feature Extraction(特徵提取)、Feature Selection(特徵選擇)和 Feature construction(特徵構造)等子問題,本章內容主要討論特徵構造的方法。

創造新的特徵是一件十分困難的事情,需要豐富的專業知識和大量的時間。機器學習應用的本質基本上就是特徵工程。 ——Andrew Ng
0x01 特徵構造介紹
時間特構造以及時間序列特徵構造的具體方法:

0x02 時間特徵構造
對於時間型數據來說,即可以把它轉換成連續值,也可以轉換成離散值。
1.連續值時間特徵
- 持續時間(單頁瀏覽時長);
- 間隔時間;
- 上次購買/點擊離現在的時長;
- 產品上線到現在經過的時長;
2.離散值時間特徵
1)時間特徵拆解
- 年;
- 月;
- 日;
- 時;
- 分;
- 數;
- 一天中的第幾分鐘;
- 星期幾;
- 一年中的第幾天;
- 一年中的第幾個周;
- 一天中哪個時間段:凌晨、早晨、上午、中午、下午、傍晚、晚上、深夜;
- 一年中的哪個季度;
程式實現
import pandas as pd # 構造時間數據 date_time_str_list = [ '2019-01-01 01:22:26', '2019-02-02 04:34:52', '2019-03-03 06:16:40', '2019-04-04 08:11:38', '2019-05-05 10:52:39', '2019-06-06 12:06:25', '2019-07-07 14:05:25', '2019-08-08 16:51:33', '2019-09-09 18:28:28', '2019-10-10 20:55:12', '2019-11-11 22:55:12', '2019-12-12 00:55:12', ] df = pd.DataFrame({'時間': date_time_str_list}) # 把字元串格式的時間轉換成Timestamp格式 df['時間'] = df['時間'].apply(lambda x: pd.Timestamp(x)) # 年份 df['年']=df['時間'].apply(lambda x: x.year) # 月份 df['月']=df['時間'].apply(lambda x: x.month) # 日 df['日']=df['時間'].apply(lambda x: x.day) # 小時 df['時']=df['時間'].apply(lambda x: x.hour) # 分鐘 df['分']=df['時間'].apply(lambda x: x.minute) # 秒數 df['秒']=df['時間'].apply(lambda x: x.second) # 一天中的第幾分鐘 df['一天中的第幾分鐘']=df['時間'].apply(lambda x: x.minute + x.hour*60) # 星期幾; df['星期幾']=df['時間'].apply(lambda x: x.dayofweek) # 一年中的第幾天 df['一年中的第幾天']=df['時間'].apply(lambda x: x.dayofyear) # 一年中的第幾周 df['一年中的第幾周']=df['時間'].apply(lambda x: x.week) # 一天中哪個時間段:凌晨、早晨、上午、中午、下午、傍晚、晚上、深夜; period_dict ={ 23: '深夜', 0: '深夜', 1: '深夜', 2: '凌晨', 3: '凌晨', 4: '凌晨', 5: '早晨', 6: '早晨', 7: '早晨', 8: '上午', 9: '上午', 10: '上午', 11: '上午', 12: '中午', 13: '中午', 14: '下午', 15: '下午', 16: '下午', 17: '下午', 18: '傍晚', 19: '晚上', 20: '晚上', 21: '晚上', 22: '晚上', } df['時間段']=df['時'].map(period_dict) # 一年中的哪個季度 season_dict = { 1: '春季', 2: '春季', 3: '春季', 4: '夏季', 5: '夏季', 6: '夏季', 7: '秋季', 8: '秋季', 9: '秋季', 10: '冬季', 11: '冬季', 12: '冬季', } df['季節']=df['月'].map(season_dict) 
2)時間特徵判斷
- 是否閏年;
- 是否月初;
- 是否月末;
- 是否季節初;
- 是否季節末;
- 是否年初;
- 是否年尾;
- 是否周末;
- 是否公共假期;
- 是否營業時間;
- 兩個時間間隔之間是否包含節假日/特殊日期;
程式實現
import pandas as pd # 構造時間數據 date_time_str_list = [ '2010-01-01 01:22:26', '2011-02-03 04:34:52', '2012-03-05 06:16:40', '2013-04-07 08:11:38', '2014-05-09 10:52:39', '2015-06-11 12:06:25', '2016-07-13 14:05:25', '2017-08-15 16:51:33', '2018-09-17 18:28:28', '2019-10-07 20:55:12', '2020-11-23 22:55:12', '2021-12-25 00:55:12', '2022-12-27 02:55:12', '2023-12-29 03:55:12', '2024-12-31 05:55:12', ] df = pd.DataFrame({'時間': date_time_str_list}) # 把字元串格式的時間轉換成Timestamp格式 df['時間'] = df['時間'].apply(lambda x: pd.Timestamp(x)) # 是否閏年 df['是否閏年'] = df['時間'].apply(lambda x: x.is_leap_year) # 是否月初 df['是否月初'] = df['時間'].apply(lambda x: x.is_month_start) # 是否月末 df['是否月末'] = df['時間'].apply(lambda x: x.is_month_end) # 是否季節初 df['是否季節初'] = df['時間'].apply(lambda x: x.is_quarter_start) # 是否季節末 df['是否季節末'] = df['時間'].apply(lambda x: x.is_quarter_end) # 是否年初 df['是否年初'] = df['時間'].apply(lambda x: x.is_year_start) # 是否年尾 df['是否年尾'] = df['時間'].apply(lambda x: x.is_year_end) # 是否周末 df['是否周末'] = df['時間'].apply(lambda x: True if x.dayofweek in [5, 6] else False) # 是否公共假期 public_vacation_list = [ '20190101', '20190102', '20190204', '20190205', '20190206', '20190207', '20190208', '20190209', '20190210', '20190405', '20190406', '20190407', '20190501', '20190502', '20190503', '20190504', '20190607', '20190608', '20190609', '20190913', '20190914', '20190915', '20191001', '20191002', '20191003', '20191004', '20191005', '20191006', '20191007', ] # 此處未羅列所有公共假期 df['日期'] = df['時間'].apply(lambda x: x.strftime('%Y%m%d')) df['是否公共假期'] = df['日期'].apply(lambda x: True if x in public_vacation_list else False) # 是否營業時間 df['是否營業時間'] = False df['小時']=df['時間'].apply(lambda x: x.hour) df.loc[((df['小時'] >= 8) & (df['小時'] < 22)), '是否營業時間'] = True df.drop(['日期', '小時'], axis=1, inplace=True) 
3.結合時間維度的聚合特徵
具體就是指結合時間維度來進行聚合特徵構造,聚合特徵構造的具體方法可以參考《聚合特徵構造以及轉換特徵構造》中的《聚合特徵構造》章節。
1)首日聚合特徵
例如:註冊首日投資總金額、註冊首日頁面訪問時長、註冊首日總點擊次數等;
2)最近時間聚合特徵
例如:最近N天APP登錄天數、最近一個月的購買金額、最近購物至今天數等;
3)區間內的聚合特徵
例如:2018年至2019年的總購買金額、每天下午的平均客流量、在某公司工作期間加班的天數等;
0x03 時間序列特徵構造
時間序列不僅包含一維時間變數,還有一維其他變數,如股票價格、天氣溫度、降雨量、訂單量等。時間序列分析的主要目的是基於歷史數據來預測未來資訊。對於時間序列,我們關心的是長期的變動趨勢、周期性的變動(如季節性變動)以及不規則的變動。
按固定時間長度把時間序列劃分成多個時間窗,然後構造每個時間窗的特徵。
1.時間序列聚合特徵
按固定時間長度把時間序列劃分成多個時間窗,然後使用聚合操作構造每個時間窗的特徵。
1)平均值
例子:歷史銷售量平均值、最近N天銷售量平均值。
2)最小值
例子:歷史銷售量最小值、最近N天銷售量最小值。
3)最大值
例子:歷史銷售量最大值、最近N天銷售量最大值。
4)擴散值
分布的擴散性,如標準差、平均絕對偏差或四分位差,可以反映測量的整體變化趨勢。
5)離散係數值
離散係數是策略數據離散程度的相對統計量,主要用於比較不同樣本數據的離散程度。
6)分布性
時間序列測量的邊緣分布的高階特效估計(如偏態係數或峰態係數),或者更進一步對命名分布進行統計測試(如標準或統一性),在某些情況下比較有預測力。
程式實現:洗髮水銷售數據
import pandas as pd # 載入洗髮水銷售數據集 df = pd.read_csv('shampoo-sales.csv') df.dropna(inplace=True) df.rename(columns={'Sales of shampoo over a three year period': 'value'}, inplace=True) # 平均值 mean_v = df['value'].mean() print('mean: {}'.format(mean_v)) # 最小值 min_v = df['value'].min() print('min: {}'.format(min_v)) # 最大值 max_v = df['value'].max() print('max: {}'.format(max_v)) # 擴散值:標準差 std_v = df['value'].std() print('std: {}'.format(std_v)) # 擴散值:平均絕對偏差 mad_v = df['value'].mad() print('mad: {}'.format(mad_v)) # 擴散值:四分位差 q1 = df['value'].quantile(q=0.25) q3 = df['value'].quantile(q=0.75) irq = q3 - q1 print('q1={}, q3={}, irq={}'.format(q1, q3, irq)) # 離散係數 variation_v = std_v/mean_v print('variation: {}'.format(variation_v)) # 分布性:偏態係數 skew_v = df['value'].skew() print('skew: {}'.format(skew_v)) # 分布性:峰態係數 kurt_v = df['value'].kurt() print('kurt: {}'.format(kurt_v)) # 輸出: mean: 312.59999999999997 min: 119.3 max: 682.0 std: 148.93716412347473 mad: 119.66666666666667 q1=192.45000000000002, q3=411.1, irq=218.65 variation: 0.47644646232717447 skew: 0.8945388528534595 kurt: 0.11622821118738624 註:
- 上面是單個時間序列的實現程式碼,多個時間序列的數據集構造特徵時需要先進行分組再計算。如IJCAI-17口碑商家客流量預測比賽中,數據集中包含多個商家的歷史銷售數據,構造特徵時需要先按商家分組,然後再構建特徵。
- 上述程式碼都是使用所有歷史數據來構造特徵,實際項目中如果待預測目標為t時刻的值,則使用t時刻之前的值來構造特徵,不同的t值都可以分別構造訓練樣本對應的特徵。 如:使用t時刻的y值作為label,則使用t-1時刻之前的y值來構造特徵;使用t-1時刻的y值作為label時,則使用t-2時刻之前的y值來構造特徵。如此類推,我們可以得到多個訓練樣本,每個樣本有多個特徵。
2.時間序列歷史特徵
1)前一(或n)個窗口的取值
例子:昨天、前天和3天前的銷售量。
2)周期性時間序列前一(或n)周期的前一(或n)個窗口的取值
例子:寫字樓樓下的快餐店的銷售量一般具有周期性,周期長度為7天,7天前和14天前的銷售量。
程式實現:洗髮水銷售數據
import pandas as pd # 載入洗髮水銷售數據集 df = pd.read_csv('shampoo-sales.csv') df.dropna(inplace=True) df.rename(columns={'Sales of shampoo over a three year period': 'value'}, inplace=True) df['-1day'] = df['value'].shift(1) df['-2day'] = df['value'].shift(2) df['-3day'] = df['value'].shift(3) df['-1period'] = df['value'].shift(1*12) df['-2period'] = df['value'].shift(2*12) display(df.head(60)) 
3.時間序列複合特徵
1)趨勢特徵
趨勢特徵可以刻畫時間序列的變化趨勢。
例子:每個用戶每天對某個Item行為次數的時間序列中,User一天對Item的行為次數/User三天對Item的行為次數的均值,表示短期User對Item的熱度趨勢,大於1表示活躍逐漸在提高;三天User對Item的行為次數的均值/七天User對Item的行為次數的均值表示中期User對Item的活躍度的變化情況;七天User對Item的行為次數的均值/ 兩周User對Item的行為次數的均值表示「長期」(相對)User對Item的活躍度的變化情況。
程式實現:
import pandas as pd # 載入洗髮水銷售數據集 df = pd.read_csv('shampoo-sales.csv') df.dropna(inplace=True) df.rename(columns={'Sales of shampoo over a three year period': 'value'}, inplace=True) df['last 3 day mean'] = (df['value'].shift(1) + df['value'].shift(2) + df['value'].shift(3))/3 df['最近3天趨勢'] = df['value'].shift(1)/df['last 3 day mean'] display(df.head(60)) 
2)窗口差異值特徵
一個窗口到下一個窗口的差異。例子:商店銷售量時間序列中,昨天的銷售量與前天銷售量的差值。
程式實現:
import pandas as pd # 載入洗髮水銷售數據集 df = pd.read_csv('shampoo-sales.csv') df.dropna(inplace=True) df.rename(columns={'Sales of shampoo over a three year period': 'value'}, inplace=True) df['最近兩月銷量差異值'] = df['value'].shift(1) - df['value'].shift(2) display(df.head(60)) 
3)自相關性特徵
原時間序列與自身左移一個時間空格(沒有重疊的部分被移除)的時間序列相關聯。
程式實現:
import statsmodels.tsa.api as smt import pandas as pd # 載入洗髮水銷售數據集 df = pd.read_csv('shampoo-sales.csv') df.dropna(inplace=True) df.rename(columns={'Sales of shampoo over a three year period': 'value'}, inplace=True) print('滯後數為1的自相關係數:{}'.format(df['value'].autocorr(1))) print('滯後數為2的自相關係數:{}'.format(df['value'].autocorr(2))) # 輸出: 滯後數為1的自相關係數:0.7194822398024308 滯後數為2的自相關係數:0.8507433352850972 除了上面描述的特徵外,時間序列還有歷史波動率、瞬間波動率、隱含波動率、偏度、峰度、瞬時相關性等特徵。
0x0FF 總結
1.時間特徵主要有兩大類:
1)從時間變數提取出來的特徵
- 如果每條數據為一條訓練樣本,時間變數提取出來的特徵可以直接作為訓練樣本的特徵使用。
例子:用戶註冊時間變數。對於每個用戶來說只有一條記錄,提取出來的特徵可以直接作為訓練樣本的特徵使用,不需要進行二次加工。
- 如果每條數據不是一條訓練樣本,時間變數提取出來的特徵需要進行二次加工(聚合操作)才能作為訓練樣本的特徵使用。
例子:用戶交易流水數據中的交易時間。由於每個用戶的交易流水數量不一樣,從而導致交易時間提取出來的特徵的數據不一致,所以這些特徵不能直接作為訓練樣本的特徵來使用。我們需要進一步進行聚合操作才能使用,如先從交易時間提取出交易小時數,然後再統計每個用戶在每個小時(1-24小時)的交易次數來作為最終輸出的特徵。
2)對時間變數進行條件過濾,然後再對其他變數進行聚合操作所產生的特徵
主要是針對類似交易流水這樣的數據,從用戶角度進行建模時,每個用戶都有不定數量的數據,因此需要對數據進行聚合操作來為每個用戶構造訓練特徵。而包含時間的數據,可以先使用時間進行條件過濾,過濾後再構造聚合特徵。
2. 時間序列數據可以從帶有時間的流水數據統計得到,實際應用中可以分別從帶有時間的流水數據以及時間序列數據中構造特徵,這些特徵可以同時作為模型輸入特徵。
例如:美團的商家銷售量預測中,每個商家的交易流水經過加工後可以得到每個商家每天的銷售量,這個就是時間序列數據。
預告:下一篇文章將介紹空間特徵構造以及文本特徵構造。
參考文獻
[1] https://machinelearning-notes.readthedocs.io/zh_CN/latest/feature/%E7%89%B9%E5%BE%81%E5%B7%A5%E7%A8%8B%E2%80%94%E2%80%94%E6%97%B6%E9%97%B4.html [2] https://www.cnblogs.com/nxf-rabbit75/p/11141944.html#_nav_12 [3] https://gplearn.readthedocs.io/en/stable/examples.html#symbolic-classifier [4] 利用 gplearn 進行特徵工程. https://bigquant.com/community/t/topic/120709 [5] Practical Lessons from Predicting Clicks on Ads at Facebook. https://pdfs.semanticscholar.org/daf9/ed5dc6c6bad5367d7fd8561527da30e9b8dd.pdf [6] Feature Tools:可自動構造機器學習特徵的Python庫. https://www.jiqizhixin.com/articles/2018-06-21-2 [7] 各種聚類演算法的系統介紹和比較. https://blog.csdn.net/abc200941410128/article/details/78541273
