作者:许梦洁 (中山大学)
Stata 连享会: 因果推断-内生性 专题 ⌚ 2020.11.12-15 主讲:王存同 (中央财经大学);司继春(上海对外经贸大学)空间计量 专题 ⌚ 2020.12.10-13 主讲:杨海生 (中山大学);范巧 (兰州大学)
一、任务描述
对2010年后49083条上市公司股权变更数据(Firm-Event 观测)分别统计每个事件发生前后15天公司:发布的临时公告数
累计超额收益(CAR)
二、数据描述
数据集总样本数2010年后的样本数上市公司股权变更记录5758449083上市公司公告记录27870262758934上市公司日超额收益97494645534947
三、解决思路
在Python 构造一个类似于 Excel 中的 countif 函数即可。具体见我上一篇博文
四、潜在问题
虽然按照上一篇文章的思路也能基本完成任务,但是程序运行非常慢,光跑一次统计窗口期公告数据的程序就要27个小时,所以必须优化程序以提高运行速度。
五、优化思路
由于全样本非常大,上篇博文的程序相当于是对每一个公司股东股权变动事件在全部的公告池(2758934条记录)中进行搜索,显然这样做是无效率的。
因此,此次程序优化的主要思路是分别拆分 49083 条公司股东股权变动事件和 2758934 条上市公司公告记录,并将两类拆分的文件对应起来。举个例子:第一步: 将股票代码在 000001 到 000049 的公司股权变动事件拆出来
第二步: 将股票代码在000001 到 000049 的公司公告拆出来形成一个小的公告搜索池,然后对这部分公司的股权事件在这个小搜索池里统计公告记录。
通过拆分文件得到的精确匹配大大减小了每一个事件的搜索范围,可以大幅提高程序运行效率。优化后跑一次同样的统计窗口期公告数据程序仅需 12 分钟,是原来运行速度的 135 倍。
六、核心代码(以统计窗口期CAR为例)
1. 初步拆分
为了保证运行效率最高,首先平均分拆 CAR 序列,遍历股票日超额收益序列数据,设定阈值为 50000,每 50000 条数据拆出一个文件,以 "CAR+编号" 为文件名 (eg: CAR109.txt)。最后共拆出了 110 个文件,Python 代码如下:
LIMIT = 50000
file_count = 0
url_list = []
with open("股票日超额收益序列.txt") as f:
for line in f:
url_list.append(line)
if len(url_list)
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” 中。Python 代码如下:
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 文件根据股票代码以及日期进行排过序,因此接下来的拆分只需逐行遍历,判断遍历到的观测股票代码与拆分节点处的股票代码的关系,如果遍历处股票代码大于当前拆分节点,则保存一个分拆文件,清空相关变量并开启下一个分拆文件。Python 代码如下:
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啦,这部分思路见上一篇博文
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天公告数22014-03-210.1928260.0639818622014-08-29-0.057021-0.03309719622014-09-16-0.031721-0.03163561622015-01-24-0.010155-0.10772231322015-01-28-0.069575-0.04520161022015-07-110.356788-0.126676151622015-07-25-0.1925250.0095171222015-08-040.019329-0.12050872922015-08-270.142061-0.048584221122015-12-070.2429670.221147111622015-12-090.3533910.276527121522015-12-160.2687260.124451172622016-07-07-0.27522-0.133624461822016-08-050.2940570.1905699822016-08-090.2950280.076121715Stata连享会 由中山大学连玉君老师团队创办,定期分享实证分析经验。
欢迎赐稿: 欢迎赐稿至StataChina@163.com。录用稿件达 三篇 以上,即可 免费 获得一期 Stata 现场培训资格。