面板資料處理太慢?一文幫你效能提升千倍

時間序列資料、截面資料和麵板資料是金融領域中常見的資料組織方式。面板資料包含了時間序列和橫截面兩個維度。在Python中,通常可以用pandas的DataFrame或numpy的二維陣列來表示。在DolphinDB中面板資料也可以用表(table)或矩陣(matrix)來表示。

本教程主要介紹如何在DolphinDB中表示和分析面板資料,包含的內容有:

面板資料的表示方法和處理函式、SQL語句處理面板資料、向量化函式處理面板資料、面板資料處理方式的效能對比

點選左側目錄可快速檢視詳細內容。

本文所有例項均基於DolphinDB 1。30。

1。 面板資料的表示方法和處理函式

DolphinDB提供了兩種方法處理面板資料:

透過SQL和向量化函式來處理用二維表表示的面板資料

透過向量化函式來處理用矩陣表示的面板資料

DolphinDB中資料表和矩陣都採用了列式儲存。以下是表和矩陣中的列常用的計算函式和二元運算子:

二元運算子:+, -, *, /, ratio, %, &&, ||, &, |, pow

序列函式:ratios, deltas, prev, next, move

滑動視窗函式:mcount,mavg, msum, mmax, mimax, mimin, mmin, mprod, mstd, mvar, mmed,mpercentile, mrank, mwavg, mwsum, mbeta, mcorr, mcovar

累計視窗函式:cumcount, cumavg, cumsum, cummax, cummin, cumprod, cumstd, cumvar, cummed, cumpercentile, cumPositiveStreak, cumrank, cumwavg, cumwsum, cumbeta, cumcorr, cumcovar

聚合函式:count, avg, sum, sum2, first, firstNot, last, lastNot, max, min, std, var, med, mode, percentile, atImax, atImin, wavg, wsum, beta, corr, covar

聚合函式(針對面板資料的每一行進行計算): rowCount, rowAvg, rowSum, rowSum2, rowProd, rowMax, rowMin, rowStd, rowVar, rowAnd, rowOr, rowXor

我們會具體在下文中透過舉例的方式讓讀者更能瞭解這些函式是如何進行面板資料操作。

2。 SQL語句處理面板資料

當使用DolphinDB的二維資料表來表示SQL的面板資料時,通常一個列儲存一個指標,譬如open,high,low,close,volume等,一行代表一個股票在一個時間點的資料。這樣的好處是,多個指標進行處理時,不再需要對齊資料。缺點是分組計算(按股票分組的時間序列計算,或者按時間分組的橫截面計算),需要先分組,SQL語句的group by/context by/pivot by子句均可用於分組。分組有一定的開銷,通常儘可能把所有的計算在一次分組內全部計算完成。

DolphinDB的SQL不僅支援SQL的標準功能,還進行了擴充套件,包括面板資料處理,非同時連線,視窗連線,視窗函式等

sym = `C`C`C`C`MS`MS`MS`IBM`IBMtimestamp = [09:34:57,09:34:59,09:35:01,09:35:02,09:34:57,09:34:59,09:35:01,09:35:01,09:35:02]price= 50。6 50。62 50。63 50。64 29。46 29。48 29。5 174。97 175。02 volume = 2200 1900 2100 3200 6800 5400 1300 2500 8800 t = table(sym, timestamp, price, volume);t;sym timestamp price volume——- ————- ———— ————C 09:34:57 50。6 2200C 09:34:59 50。62 1900C 09:35:01 50。63 2100C 09:35:02 50。64 3200MS 09:34:57 29。46 6800MS 09:34:59 29。48 5400MS 09:35:01 29。5 1300IBM 09:35:01 174。97 2500IBM 09:35:02 175。02 8800

2。1 context by

context by 是 DolphinDB 獨有的功能,是對標準SQL語句的拓展,我們可以透過context by子句實現的分組計算功能來簡化對資料面板的操作。

SQL的group by子句將資料分成多組,每組產生一個值,也就是一行。因此使用group by子句後,行數一般會大大減少。

在對面板資料進行分組後,每一組資料通常是時間序列資料,譬如按股票分組,每一個組內的資料是一個股票的價格序列。處理面板資料時,有時候希望保持每個組的資料行數,也就是為組內的每一行資料生成一個值。例如,根據一個股票的價格序列生成回報序列,或者根據價格序列生成一個移動平均價格序列。其它資料庫系統(例如SQL Server,PostGreSQL),用視窗函式(window function)來解決這個問題。

DolpinDB引入了context by子句來處理面板資料。context by與視窗函式相比,除了語法更簡潔,設計更系統化(與group by和pivot by一起組成對分組資料處理的三個子句)以外,表達能力上也更強大,具體表現在:

不僅能與select配合在查詢中使用,也可以與update配合更新資料。

絕大多數資料庫系統在視窗函式中只能使用表中現有的欄位分組。context by子句可以使用任何現有欄位和計算欄位。

視窗函式僅限於少數幾個函式。context by不僅不限制使用的函式,而且可以使用任意表達式,譬如多個函式的組合。

context by可以與having子句配合使用,以過濾每個組內部的行。

(1) 按股票程式碼進行分組,應用序列函式計算每一隻股票的前後交易量比率,進行對比:

select timestamp, sym, price, ratios(volume) ,volume from t context by sym;timestamp sym price ratios_volume volume————- ——- ———— ——————- ————09:34:57 C 50。6 2200 09:34:59 C 50。62 0。86 1900 09:35:01 C 50。63 1。106 2100 09:35:02 C 50。64 1。52 3200 09:35:01 IBM 174。97 2500 09:35:02 IBM 175。02 3。52 8800 09:34:57 MS 29。46 6800 09:34:59 MS 29。48 0。79 5400 09:35:01 MS 29。5 0。24 1300

(2) 結合滑動視窗函式,計算每隻股票在3次資料更新中的平均價格:

select *, mavg(price,3) from t context by sym;sym timestamp price volume mavg_price ——- ————- ———— ———— ——————-C 09:34:57 50。60 2200 C 09:34:59 50。62 1900 C 09:35:01 50。63 2100 50。62C 09:35:02 50。64 3200 50。63IBM 09:35:01 174。97 2500 IBM 09:35:02 175。02 8800 MS 09:34:57 29。46 6800 MS 09:34:59 29。48 5400 MS 09:35:01 29。50 1300 29。48

(3) 結合累計視窗函式,計算每隻股票在每一次的資料更新中最大交易量:

select timestamp, sym, price,volume, cummax(volume) from t context by sym;timestamp sym price volume cummax_volume————- ——- ———— ———— ——————-09:34:57 C 50。6 2200 2200 09:34:59 C 50。62 1900 2200 09:35:01 C 50。63 2100 2200 09:35:02 C 50。64 3200 3200 09:35:01 IBM 174。97 2500 2500 09:35:02 IBM 175。02 8800 8800 09:34:57 MS 29。46 6800 6800 09:34:59 MS 29。48 5400 6800 09:35:01 MS 29。5 1300 6800

(4) 應用聚合函式,計算每隻股票在每分鐘中的最大交易量:

pivot by是DolphinDB 的獨有功能,是對標準SQL語句的拓展,可將資料表中某列的內容按照兩個維度整理,產生資料表或矩陣。sym timestamp price volume max_volume——- ————- ———— ———— ——————C 09:34:57 50。61 2200 2200 C 09:34:59 50。62 1900 2200 C 09:35:01 50。63 2100 3200 C 09:35:02 50。64 3200 3200 IBM 09:35:01 174。97 2500 8800 IBM 09:35:02 175。02 8800 8800 MS 09:34:57 29。46 6800 6800 MS 09:34:59 29。48 5400 6800 MS 09:35:01 29。5 1300 1300

2。2 pivot by

pivot by是 DolphinDB 的獨有功能,是對標準SQL語句的拓展,可將資料表中某列的內容按照兩個維度整理,產生資料表或矩陣。

透過應用pivot by子句,可以對於資料表t進行重新排列整理:每行為一秒鐘,每列為一隻股票,既能夠了解單個股票每個時刻的變化,也可以瞭解各股票之間的差異。

如:對比同一時間段不同股票的交易價格:

select price from t pivot by timestamp, sym;timestamp C IBM MS————- ——- ———— ——-09:34:57 50。6 29。4609:34:59 50。62 29。4809:35:01 50。63 174。97 29。509:35:02 50。64 175。02

pivot by還可以與聚合函式一起使用。比如,將資料中每分鐘的平均收盤價轉換為資料表:

select avg(price) from t where sym in `C`IBM pivot by minute(timestamp) as minute, sym;minute C IBM———— ———— ————-09:34m 50。6109:35m 50。635 174。995

3。 向量化函式處理面板資料

當使用DolphinDB的矩陣來表示面板資料時,資料按時間序列和橫截面兩個維度進行排列。

對矩陣表示的面板資料進行分析時,如:每行是按時間戳排序的時間點,每列是一隻股票,我們既可以對某一隻股票進行多個時間點的動態變化分析,也可以瞭解多個股票之間在某個時點的差異情況。

向量化函式panel可將一列或多列資料轉換為矩陣。例如,將資料表t中的price列轉換為一個矩陣:

price = panel(t。timestamp, t。sym, t。price);price; C IBM MS ——- ———— ——-09:34:57|50。60 29。4609:34:59|50。62 29。4809:35:01|50。63 174。97 29。509:35:02|50。64 175。02

以下指令碼將price與volume列分別轉換為矩陣。返回的結果是一個元組,每個元素對應一列轉換而來的矩陣。

price, volume = panel(t。timestamp, t。sym, [t。price, t。volume]);

使用panel函式時,可以指定結果矩陣的行與列的標籤。這裡需要注意,行與列的標籤均需嚴格升序。例如:

rowLabel = 09:34:59。。09:35:02;colLabel = [“C”, “MS”];volume = panel(t。timestamp, t。sym, t。volume, rowLabel, colLabel);volume; C MS —— ——09:34:59|1900 540009:35:00| 09:35:01|2100 130009:35:02|3200

使用rowNames和colNames函式可以獲取panel函式返回的矩陣的行和列標籤:

volume。rowNames();volume。colNames();

如果後續要對面板資料做一步的計算和處理,推薦使用矩陣來表示面板資料。這是因為矩陣天然支援向量化操作和二元操作,計算效率會更高,程式碼會更簡潔。

3。1 矩陣操作示例

下面舉例一些處理用矩陣表示的面板資料時常用的操作。

(1) 透過序列函式,對每個股票的相鄰價格進行比較。

price = panel(t。timestamp, t。sym, t。price);deltas(price); C IBM MS —— —— ——-09:34:57| 09:34:59|0。02 0。0209:35:01|0。01 0。0209:35:02|0。01 0。05

(2) 結合滑動視窗函式,計算每隻股票在每2次資料更新中的平均價格。

mavg(price,2); C IBM MS ———— ———— ——————-09:34:57| 09:34:59|50。61 29。4709:35:01|50。63 174。97 29。4909:35:02|50。63 175。00 29。50

(3) 結合累計視窗函式,計算每隻股票中價格的排序。

cumrank(price); C IBM MS - ——- ——09:34:57|0 0 09:34:59|1 1 09:35:01|2 0 2 09:35:02|3 1

(4) 透過聚合函式,得到每隻股票中的最低價格。

min(price);[50。60,174。97,29。46]

(5) 透過聚合函式,得到每一個同時間段的最低股票價格。

rowMin(price);[29。46,29。48,29。5,50。64]

3。2 對齊矩陣的二次運算

DolphinDB提供了兩種擴充套件的資料結構來支援面板資料的對齊運算:indexedMatrix和indexedSeries。

普通矩陣進行二元運算時,按照對應元素分別進行計算,需要保持維度(shape)一致,而indexedMatrix和indexedSeries 幫助矩陣或向量進行二元運算時根據行列標籤(index)自動對齊,對維度沒有硬性要求。

indexedMatrix和indexedSeries支援的二元運算子和函式有:

(1)算術運算子和函式:+, -, *, /(整除), ratio, %(mod), pow

(2)邏輯運算子和函式:and, or, bitXor, &, |

(3)滑動視窗函式:mwavg, mwsum, mbeta, mcorr, mcovar

(4)累計視窗函式:cumwavg, cumwsum, cumbeta, cumcorr, cumcovar

(5)聚合函式:wavg, wsum, beta, corr, covar

3。2。1 indexedMatrix

indexedMatrix是特殊的矩陣,它將矩陣的行與列標籤作為索引。indexedMatrix進行二元運算時,系統對根據行與列標籤將多個矩陣對齊,只對行列標籤相同的資料進行計算。使用setIndexedMatrix!函式可以將普通矩陣設定為indexedMatrix。比如:

m=matrix(1。。5, 6。。10, 11。。15);m。rename!(2020。01。01。。2020。01。05, `A`B`C);m。setIndexedMatrix!(); A B C - —— ——2020。01。01|1 6 112020。01。02|2 7 122020。01。03|3 8 132020。01。04|4 9 142020。01。05|5 10 15n=matrix(1。。5, 6。。10, 11。。15);n。rename!(2020。01。02。。2020。01。06, `B`C`D);n。setIndexedMatrix!(); B C D - —— ——2020。01。02|1 6 112020。01。03|2 7 122020。01。04|3 8 132020。01。05|4 9 142020。01。06|5 10 15m+n; A B C D - —— —— -2020。01。01|2020。01。02| 8 182020。01。03| 10 202020。01。04| 12 222020。01。05| 14 242020。01。06|

3。2。2 indexedSeries

可以使用indexedSeries函式生成帶有索引的向量。例如:

index = 2008。01。02。。2008。01。31;value = 1。。30;indexedSeries(index, value);

(1)indexedSeries之間的對齊運算

兩個indexedSeries進行二元操作,會根據index進行對齊再做計算。

index1 = 2020。11。01。。2020。11。06;value1 = 1。。6;s1 = indexedSeries(index1, value1);index2 = 2020。11。04。。2020。11。09;value2 =4。。9;s2 = indexedSeries(index2, value2);s1+s2; #0 ——2020。11。01|2020。11。02|2020。11。03|2020。11。04|82020。11。05|102020。11。06|122020。11。07|2020。11。08|2020。11。09|

(2)indexedSeries和indexedMatrix之間的對齊運算

indexedSeries與indexedMatrix進行二元操作,會根據行標籤進行對齊,indexedSeries與indexedMatrix的每列進行計算。

m1=matrix(1。。6, 11。。16);m1。rename!(2020。11。04。。2020。11。09, `A`B);m1。setIndexedMatrix!();m1; A B - ——2020。11。04|1 112020。11。05|2 122020。11。06|3 132020。11。07|4 142020。11。08|5 152020。11。09|6 16s1; #0 ——2020。11。01|12020。11。02|22020。11。03|32020。11。04|42020。11。05|52020。11。06|6m1 + s1; A B - ——2020。11。01|2020。11。02|2020。11。03|2020。11。04|5 152020。11。05|7 172020。11。06|9 192020。11。07|2020。11。08|2020。11。09|

3。3 重取樣和頻度轉換

DolphinDB提供了resample和asfreq函式,用於對有時間型別索引的indexedSeries或者indexedMatrix進行重取樣和頻度轉換。

其實現目的是提供使用者一個對常規時間序列資料重新取樣和頻率轉換的便捷的方法。

3。3。1 resample(重取樣)

重取樣是指將時間序列的頻度轉換為另一個頻度。重取樣時必須指定一個聚合函式對資料進行計算。

降低取樣頻率為月:

index=2020。01。01。。2020。06。30;s=indexedSeries(index, take(1,size(index)));s。resample(“M”,sum); #0 ——2020。01。31|312020。02。29|292020。03。31|312020。04。30|302020。05。31|312020。06。30|30

3。3。2 asfreq(頻率轉換)

asfreq函式轉換給定資料的時間頻率。與resample函式不同,asfreq不能使用聚合函式對資料進行處理。

s。asfreq(“M”); #0 ——2020。01。31|12020。02。29|12020。03。31|12020。04。30|12020。05。31|12020。06。30|1

3。3。3 NULL值的處理

在重取樣和頻率轉換中,可能需要對結果的NULL值進行處理。

(1)刪除NULL值

DolphinDB提供dropna函式,用於刪除向量中的NULL值,或矩陣中包含NULL值的行或列。

dropna的語法如下:

dropna(X, [byRow=true], [thresh])

X是向量或矩陣。

byRow是布林值。byRow=true表示按行刪除,byRow=false表示按列刪除。

thresh是整數。若一行(列)中非NULL元素少於該值,則刪除該行(列)。

index=2020。01。01。。2020。06。30;m=matrix(take(3 4 5 NULL NULL 1 2, size(index)), take(1 2 3 4 5 NULL NULL, size(index)));m。rename!(index, `A`B);m。setIndexedMatrix!();m。asfreq(“M”); A B - -2020。01。31|5 32020。02。29| 42020。03。31|22020。04。30|4 22020。05。31| 52020。06。30|2m。asfreq(“M”)。dropna(); A B - -2020。01。31|5 32020。04。30|4 2

(2)填充NULL值

DolphinDB提供多種填充NULL值的函式:向前填充ffill,向後填充bfill,特定值填充nullFill和線性插值interpolate。例如:

m。asfreq(“M”)。ffill(); A B - -2020。01。31|5 32020。02。29|5 42020。03。31|2 42020。04。30|4 22020。05。31|4 52020。06。30|2 5

(3)替換NULL值

DolphinDB提供高階函式withNullFill,可以用特定值替換Null值參與計算。

withNullFill函式的語法如下:

withNullFill(func, x, y, fillValue)

func是一個DolphinDB內建函式,須為雙目運算子,例如+, -, *, /, ratio, %, pow, and, or 等。

x和y可以是向量或矩陣。

fillValue是一個標量。

若x與y中相同位置的元素只有一個為NULL,使用fillValue替換NULL值參與計算。如果x和y相同位置的元素均為NULL,返回NULL。

s1=m。asfreq(“M”)[`A];s2=m。asfreq(“M”)[`B];s1+s2; #0 ——2020。01。31|82020。02。29|2020。03。31|2020。04。30|62020。05。31|2020。06。30|withNullFill(add, s1, s2, 0); #0 ——2020。01。31|82020。02。29|42020。03。31|22020。04。30|62020。05。31|52020。06。30|2

4。 面板資料處理方式的對比

下面我們會以一個更為複雜的實際例子演示如何高效的解決面板資料問題。著名論文101 Formulaic Alphas給出了世界頂級量化對沖基金WorldQuant所使用的101個因子公式,其中裡面80%的因子仍然還行之有效並被運用在實盤專案中。

這裡選取了WorldQuant公開的Alpha98因子的表示式。

alpha_098 = (rank(decay_linear(correlation(((high_0+low_0+open_0+close_0)*0。25), sum(mean(volume_0,5), 26。4719), 4。58418), 7。18088)) -rank(decay_linear(ts_rank(ts_argmin(correlation(rank(open_0), rank(mean(volume_0,15)), 20。8187), 8。62571),6。95668), 8。07206)))

為了更好的對比各個處理方式之間的差異,選擇了一年的股票每日資料,涉及的原始資料量約為35萬條。

以下是指令碼測試所需要的資料, 輸入資料為包含以下欄位的table:

ts_code:股票程式碼

trade_date:日期

amount:交易額

vol:成交量

vwap:成交量的加權平均價格

open:開盤價格

close:收盤價格

4。1 DolphinDB SQL 與 向量化函式處理面板資料的對比

我們會分別使用DolphinDB SQL語句和矩陣來實現計算Alpha98因子。

DolphinDB SQL語句實現Alpha98因子計算的指令碼如下:

def alpha98(stock){ t = select ts_code, trade_date, (stock。amount * 1000) /(stock。vol * 100 + 1) as vwap, open, mavg(vol, 5) as adv5, mavg(vol,15) as adv15 from stock context by ts_code update t set rank_open = rank(open), rank_adv15 = rank(adv15) context by trade_date update t set decay7 = mavg(mcorr(vwap, msum(adv5, 26), 5), 1。。7), decay8 = mavg(mrank(9 - mimin(mcorr(rank_open, rank_adv15, 21), 9), true, 7), 1。。8) context by ts_code return select ts_code, trade_date, rank(decay7)-rank(decay8) as A98 from t context by trade_date }

DIR=“/home/llin/hzy/server1/support”t=loadText(DIR+“/tushare_daily_data。csv”)timer alpha98(t)

以下是在DolphinDB中透過向量化函式來計算98號因子的指令碼:

def myrank(x){ return rowRank(x)\x。columns()}def alpha98(vwap, open, vol){ return myrank(mavg(mcorr(vwap, msum(mavg(vol, 5), 26), 5), 1。。7)) - myrank(mavg(mrank(9 - mimin(mcorr(myrank(open), myrank(mavg(vol, 15)), 21), 9), true, 7), 1。。8))}

DIR=“/home/llin/hzy/server1/support”t=loadText(DIR+“/tushare_daily_data。csv”)timer vwap, open, vol = panel(t。trade_date, t。ts_code, [(t。amount * 1000) /(t。vol * 100 + 1), t。open, t。vol])timer res = alpha98(vwap, open, vol)

透過兩個Alpha98因子指令碼的對比,我們可以發現用向量化函式來實現Alpha98因子的指令碼會更加簡潔一點。 因為Alpha98因子在計算過程中用到了截面資料,也用到了大量時間序列的計算,即在計算某天股票某一天的因子中,既要用到該股票的歷史資料,也要用到當天所有股票的資訊,對資訊量的要求很大, 而矩陣形式的面板資料是截面資料和時間序列資料綜合起來的一種資料型別,可以支援股票資料按兩個維度進行排列, 所以在實現Alpha98因子計算中,不需要多次對中間資料或輸出資料進行多次維度轉換,簡化了計算邏輯。對比使用SQL語句執行計算,在實現Alpha98因子計算的過程中,進行函式巢狀的同時還需要多次進行分組計算來處理資料。用panel函式來處理面板資料,明顯計算效率會更高,程式碼會更簡潔。

在效能測試方面,我們使用單執行緒計算,SQL語句計算Alpha98因子耗時232ms。而panel函式生成面板資料耗時37ms,計算Alpha98因子耗時141ms。兩者的耗時差異不大,矩陣方式可能略勝一籌。

但是向量化函式處理面板資料也有侷限性, 矩陣的面板資料無法進行再次分組,單值模型格式不夠直觀,而SQL支援多列分組,可以聯合查詢多個欄位的資訊,適用於海量資料的平行計算。

在處理面板資料時,客戶可根據自身對資料的分析需求,來選擇不同的方法處理面板資料。

4。2 DolphinDB與pandas處理面板資料的效能對比:

pandas實現alpha98因子的指令碼如下:

vwap=conn。run(“vwap1”);vwap。set_index(“trade_date”, inplace=True)open=conn。run(“open1”);vol=conn。run(“vol1”);open。set_index(“trade_date”, inplace=True)vol。set_index(“trade_date”, inplace=True)def myrank(x): return ((x。rank(axis=1,method=‘min’))-1)/x。shape[1]def wavg(x, weight): return (x*weight)。sum()/weight。sum()def imin(x): return np。where(x==min(x))[0][0]def rank(x): s = pd。Series(x) return (s。rank(ascending=True, method=“min”)[len(s)-1])-1def alpha98(vwap, open, vol): return myrank(vwap。rolling(5)。corr(vol。rolling(5)。mean()。rolling(26)。sum())。rolling(7)。apply(wavg, args=[np。arange(1, 8)])) - myrank((9 - myrank(open)。rolling(21)。corr(myrank(vol。rolling(15)。mean()))。rolling(9)。apply(imin))。rolling(7)。apply(rank)。rolling(8)。apply(wavg, args=[np。arange(1, 9)]))

start_time = time。time()re=alpha98(vwap, open, vol)print(“——- %s seconds ——-” % (time。time() - start_time))

使用pandas計算,Alpha98耗時290s,而使用矩陣實現計算僅耗時141ms,效能相差千倍。

效能上的巨大差異,主要得益於DolphinDB內建了許多與時序資料相關的函式,並進行了最佳化,效能優於其它系統1~2個數量級, 比如上面使用到的mavg、mcorr、mrank、mimin、msum等計算滑動視窗函式。 尤其實在計算測試二元滑動視窗(mcorr)中,DolphinDB的計算耗時0。6秒,pandas耗時142秒,效能相差200倍以上。為了避免計算結果的偶然性,我們使用了十年的股市收盤價資料,涉及的原始資料量約為530萬條,對比結果是連續執行十次的耗時ms。

就整體而言,在Alpha98因子的計算中,DolphinDB會出現效能上的斷層式優勢是有跡可循的。