任务描述:
对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 |