百萬級數據分析通過拆分文件將程序運行速度提高135倍

任務描述:

對2010年後49083條上市公司股權變更數據(Firm-Event 觀測)分別統計每個事件發生前後15天公司:
- 發佈的臨時公告數
- 累計超額收益(CAR)

數據描述:

數據集 總樣本數 2010年後的樣本數
上市公司股權變更記錄 57584 49083
上市公司公告記錄 2787026 2758934
上市公司日超額收益 9749464 5534947

解決思路

Python 構造一個類似於 Excel 中的 countif 函數即可。具體見我上一篇博文百萬級大樣本中的countif實現

問題

雖然按照上一篇文章的思路也能基本完成任務,但是程序運行非常慢,光跑一次統計窗口期公告數據的程序就要27個小時,所以必須優化程序以提高運行速度。

優化思路

由於全樣本非常大,上篇博文的程序相當於是對每一個公司股東股權變動事件在全部的公告池(2758934條記錄)中進行搜索,顯然這樣做是無效率的。因此此次程序優化的主要思路是分別拆分 49083 條公司股東股權變動事件和 2758934 條上市公司公告記錄,並將兩類拆分的文件對應起來。舉個栗子,將股票代碼在000001到000049的公司股權變動事件拆出來,再將股票代碼在000001到000049的公司公告拆出來形成一個小的公告搜索池,然後對這部分公司的股權事件在這個小搜索池裏統計公告記錄。通過拆分文件得到的精確匹配大大減小了每一個事件的搜索範圍,可以大幅提高程序運行效率。優化後跑一次同樣的統計窗口期公告數據程序僅需12分鐘,是原來運行速度的135倍。

核心代碼(以統計窗口期CAR爲例)

1. 初步拆分

爲了保證運行效率最高,首先平均分拆CAR序列,遍歷股票日超額收益序列數據,設定閾值爲50000,每50000條數據拆出一個文件,以"CAR+編號"爲文件名(eg: CAR109.txt)。最後共拆出了110個文件:

LIMIT = 50000
file_count = 0
url_list = []

with open("股票日超額收益序列.txt") as f:
    for line in f:
        url_list.append(line)
        if len(url_list)<LIMIT:
            continue
        file_name = str(file_count)+".txt"
        with open(file_name,'w') as file:
            for url in url_list[:-1]:
                file.write(url)
            file.write(url_list[-1].strip())
            url_list = []
            file_count += 1

2. 根據分拆文件記錄拆分股票節點

遍歷拆出來的110個CAR文件,分別記錄每個文件最後一個觀測的股票代碼,並逐條寫入 “拆分節點.txt” 中。

import pandas as pd

for i in range(111):
    file = "CAR"+repr(i)+".txt"
    data = pd.read_table(file, header=None, encoding='utf-8', delim_whitespace=True)
    data.columns = ['stkcd', '日期序列', '日超額收益']
    stkcdlist = data.loc[:,'stkcd']
    end = str(stkcdlist[len(stkcdlist)-1])
    with open("拆分節點.txt",'a') as f:
        f.write(end+"\n")
        print(end)
f.close()

3. 根據拆分節點拆分事件列表並再拆分CAR列表

由於初步拆分是根據樣本數拆分,因此出現了同一只股票不同日期的CAR會被拆到兩個不同文件的情況,十分不利於後面股權變更文件與CAR文件拆分後實現完美匹配。因此需要根據第二步得到拆分節點處的股票代碼對CAR列表再拆分,並同時拆分股權變更列表。經過這一步後拆分後的兩種文件就可以實現精確匹配。
此外,由於事先已經對股權變更文件以及CAR文件根據股票代碼以及日期進行排過序,因此接下來的拆分只需逐行遍歷,判斷遍歷到的觀測股票代碼與拆分節點處的股票代碼的關係,如果遍歷處股票代碼大於當前拆分節點,則保存一個分拆文件,清空相關變量並開啓下一個分拆文件。

import pandas as pd

dataf = pd.read_table("拆分節點.txt",header=None,encoding='utf-8',delim_whitespace=True)
dataf.columns = ['節點']

LIMIT = dataf.loc[:,'節點']
file_count = 0
url_list = []
line_count = 0

#這裏的分拆文件也可以是公司股權變更事件
with open("股票日超額收益序列.txt") as f:
    for line in f:
        url_list.append(line)
        line_count += 1
        currentstkcd = int(line.split("\t")[0])
        print(currentstkcd)
        try:
            print(LIMIT[file_count])
            if (currentstkcd < LIMIT[file_count]):
                continue
        except:
            print("已經是最後一個觀測")
        file_name = "CAR"+str(file_count)+".txt"
        print(file_name)
        with open(file_name,'w') as file:
            try:
                file.write(left)
            except:
                pass
            for url in url_list[:-1]:
                file.write(url)
            left = url_list[-1]
            url_list = []
            file_count += 1

4. 基於拆分後的事件列表和日期序列統計數據

一一對應地拆完大文件之後就可以在縮小的搜索範圍裏countif啦,這部分思路見上一篇博文百萬級大樣本中的countif實現。跑完49083條數據的結果只需要12分鐘,簡直是飛一般的感覺(`・ω・´)。

import pandas as pd
from datetime import *

timespan = timedelta(days=1)

def getlist(add):
    data = pd.read_table(add,encoding='utf-8',delim_whitespace=True)
    data.columns=['stkcd','日期序列','日超額收益']
    return(data)

def 區間計數(股票代碼,減持日期,前置窗口長度,後置窗口長度):
    減持時間戳 = datetime.strptime(減持日期,"%Y-%m-%d")
    開始日期 = (減持時間戳-timespan*前置窗口長度).strftime("%Y-%m-%d")
    print(開始日期)
    結束日期 = (減持時間戳+timespan*後置窗口長度).strftime("%Y-%m-%d")
    print(結束日期)
    clist = data.loc[(data['stkcd'] == 股票代碼) & (data['日期序列'] <= 結束日期) & (data['日期序列'] >= 開始日期), '日超額收益']
    區間CAR = sum(clist)
    return(區間CAR)

for i in range(111):
    with open("15天CAR統計結果.txt",'a') as g:
        try:
            f = open("事件" + repr(i)+".txt",'r')
            data = getlist("CAR" +repr(i)+".txt")
            lines = f.readlines()
            for line in lines:
                stkcd = int(line.split(',')[0].split("\n")[0])
                print(stkcd)
                eventdate = line.split(',')[1].split("\n")[0]
                事件前15天CAR = 區間計數(stkcd, eventdate, 15, 0)
                事件後15天CAR = 區間計數(stkcd, eventdate, 0, 15)
                print([stkcd, eventdate, 事件前15天CAR, 事件後15天CAR])
                g.write(','.join([repr(stkcd), eventdate, repr(事件前15天CAR), repr(事件後15天CAR)])+'\n')
            f.close()
        except:
            print("skip "+repr(i))

統計結果樣例

股票代碼 事件日期 事件前15天CAR 事件後15天CAR 事件前15天公告數 事件後15天公告數
2 2014-03-21 0.192826 0.06398 18 6
2 2014-08-29 -0.057021 -0.033097 19 6
2 2014-09-16 -0.031721 -0.031635 6 16
2 2015-01-24 -0.010155 -0.107722 3 13
2 2015-01-28 -0.069575 -0.045201 6 10
2 2015-07-11 0.356788 -0.126676 15 16
2 2015-07-25 -0.192525 0.0095 17 12
2 2015-08-04 0.019329 -0.120508 7 29
2 2015-08-27 0.142061 -0.048584 22 11
2 2015-12-07 0.242967 0.221147 11 16
2 2015-12-09 0.353391 0.276527 12 15
2 2015-12-16 0.268726 0.124451 17 26
2 2016-07-07 -0.27522 -0.133624 46 18
2 2016-08-05 0.294057 0.190569 9 8
2 2016-08-09 0.295028 0.076121 7 15
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章