前言
本文是刊载于《中国工业经济》2017年第12期《管理层讨论与分析披露的信息含量与股价崩盘——基于文本向量化方法的研究》关于文本分析构建变量的阅读笔记。原文借鉴Hanley and Hoberg (2010)的研究思路,基于文本向量化的信息构建回归模型测量文本的MD&A信息含量。本笔记主要记录指标构建思路,并且运用Python和Stata进行粗略复刻。
信息含量的定义
上市公司的MD&A不仅包括公司经营状况等历史信息,也包括与其他公司相似的信息,如外部环境、市场格局、风险因素等内容。一方面,所有上市公司都处于相同的宏观经济环境、风险因素和政治、政策背景之下;另一方面,同一行业的上市公司面临着相似的产业政策、竞争环境和市场特征。因此,上市公司的MD&A不可避免地在某种程度上与同行业其他上市公司以及市场其他行业上市公司存在一定相似性,甚至可能有直接参考其他公司MD&A的表述。
可以将与行业其他公司或其他行业的公司重复或相似的信息定义为不具有信息含量的内容,同时将不同的信息定义为真正具有信息含量的内容,简称为信息含量
如何定义重复或相似信息?
上市公司MD&A的用词种类以及频率与行业其他公司或其他行业的公司在多大程度重合。
指标构建过程
-
剔除金融行业、ST和*ST类企业,以及上市时间不足一年的企业
-
从年报中提取MD&A文本内容
-
分词处理,语料清洗
-
文本向量化
-
向量标准化
-
构建回归模型
复刻过程
数据准备
# 读取MD&A内容
mda = pd.read_excel('管理层讨论与分析.xls', sheet_name = 0)
# 读取行业数据
industry = pd.read_excel('证监会2012年版行业分类.xlsx',sheet_name = 0)
# 与行业数据进行合并
data = pd.merge(mda, industry,on=['股票代码','会计年度'], how = 'inner')
# 仅作剔除金融行业处理
data = data[~data["industrycode2"].str.contains("J")]
# 仅处理2019年的文本
data = data[data["会计年度"] == 2019]
# 重置索引
data.reset_index(drop=True)
设定分词及清理停用词函数
import jieba
import re
stoplist = [i.strip() for i in open('停用词.txt', encoding = 'utf-8').readlines()]
def m_cut(intxt):
word = [w for w in jieba.cut(intxt) if w not in stoplist and len(w) > 1 and not re.match('^[a-z|A-Z|0-9|.]*$',w)]
strword = " ".join(word)
return strword
对MD&A分词处理
data['cut'] = data['经营讨论与分析内容'].apply(m_cut)
文本向量化
词袋模型:将所有文档的词条生成语料库,对每个词条进行编号,统计每份MD&A出现的词频,生成每份MD&A的词频矩阵,格式类似于[0, 1, 4, 0, 3,…0, 5],表示对于公司 i i i的MD&A文本中,编号为1的词出现0次,编号为2的词出现1次,以此类推。
from sklearn.feature_extraction.text import CountVectorizer
# 在50份以上以及在1000份MD&A出现term的才保留,看实际情况处理
countvec = CountVectorizer(min_df = 50, max_df = 1000)
# 生成稀疏bow矩阵
res = countvec.fit_transform(data.cut)
# 生成标准文档词条矩阵
doc_term_matrix = res.toarray()
向量标准化
为了避免文本长度不同导致词频结果不可比问题,需要对以上向量进行标准化处理,即向该向量除以该文本中词条的总数。
import numpy as np
def normalizer(vec):
denom = np.sum(vec)
return [el / denom for el in vec]
doc_term_matrix_normalizer = []
for vec in doc_term_matrix:
doc_term_matrix_normalizer.append(normalizer(vec))
宽数据转换为长数据,并计算行业标准化向量和市场标准化向量
以上的得到的结果为宽数据,但是我们回归的数据格式为长数据,需要进行转换。
公司 i i i的MD&A标准化向量 N o r m i Norm_i Normi
行业标准化向量 N o r m I Norm_I NormI:将公司i所在行业除之该公司之外其他所有公司的标准化向量的算术平均得到的向量
市场标准化向量 N o r m M Norm_M NormM:将公司i所在行业之外其他行业所有公司的标准化向量的算术平均得到的向量
for index in range(0,3568): # 2019年公司数量
df_code = df[index:index+1]
df_code = df_code.melt(id_vars=['code','year','ind'], # 要保留的字段
var_name="wordid", # 拉长的分类变量
value_name="freq") # 拉长的度量值名称
# 计算行业标准化向量
dct_ind = dict(df[df["ind"] == df.iloc[index]["ind"]][df["code"] != df.iloc[index]["code"]].iloc[:,0:7308].mean(axis=0))
lt_ind = [dct_ind]
df_ind = pd.DataFrame(lt_ind)
df_ind = df_ind.melt(var_name="wordid",
value_name="freq_ind")
# 计算市场标准化向量
dct_market_ind = dict(df[df["ind"] != df.iloc[index]["ind"] ].iloc[:,0:7308].mean(axis=0))
lt_market_ind = [dct_market_ind]
df_market_ind = pd.DataFrame(lt_market_ind)
df_market_ind = df_market_ind.melt(var_name="wordid",
value_name="freq_market_ind")
# 合并数据
df_code["freq_ind"] = list(df_ind["freq_ind"])
df_code["freq_market_ind"] = list(df_market_ind["freq_market_ind"])
# 保存每家公司的词条文件
df_code.to_excel("F:/Rawdata/管理层讨论与分析/文档词条矩阵/{}词条.xls".format(df.iloc[index]["code"]),index=None)
、
构建回归模型
N o r m i = α 0 + α 1 × N o r m I + α 2 × N o r m M + ε i Norm_i=\alpha_0+\alpha_1\times Norm_I+\alpha_2\times Norm_M+ \varepsilon_i Normi=α0+α1×NormI+α2×NormM+εi
其中, α 1 \alpha_1 α1代表公司 i i i的MD&A信息中能够被同行业其他公司所解释的部分, α 2 \alpha_2 α2代表公司 i i i的MD&A信息中能够被市场其他行业公司所解释的部分,残差 ε i \varepsilon_i εi为行业和市场信息所不能解释的部分。将残差向量各维度绝对值之和定义为信息含量( I n f o Info Info)。
小结
从回归结果可以看出,行业和市场信息较好地解释了公司MD&A的信息,说明在一定程度上公司的部分文本内容与行业和市场重合了,而残差部分可能包含的未重合的信息含量,因此可以使用残差度量公司MD&A的信息含量。需要指出的是,本文在语料的细节处理上略显粗糙,详细的信息需要进一步复核分析。此外,由于目前笔者知识限制,以上的处理过程可能存在一些问题或者有更简便的方法,欢迎读者指出阅读过程中发现的问题,分享建议!
以上是笔者的第一篇推送文章。作为经管在读研究生兴趣驱动,未来会不定期推送经管相关Python和Stata数据分析和文本挖掘学习内容和笔记。在学习中实践,自娱自乐!
参考文献
[1]孟庆斌,杨俊华,鲁冰.管理层讨论与分析披露的信息含量与股价崩盘风险——基于文本向量化方法的研究[J]. 中国工业经济, 2017(12): 132-150.
[2]Hanley, K. W., and G. Hoberg. The Information Content of IPO Prospectuses[J]. Review of Financial Studies, 2010, 23(7): 2821-2864.