管理世界 | 使用md&a数据中计算 「企业融资约束指标」
Tips: 为了更好的阅读体验,建议阅读本文博客版, 链接地址
https://textdata.cn/blog/2024-12-31-using-regex-to-compute-the-financial_constraints/
一、技术路线
[工作量]
1. 代码130+行
2. 调试时间 3 小时, 运行时间 20 小时
[内容]
1. 设计正则表达式, 识别企业融资约束
2. 构建企业管理层讨论与分析文本向量(标准化) Vec_it
3. 构建板块(沪、深)文本向量(标准化)BoardVec_bt
4. 构建行业文本向量(标准化) IndustryVec_it
5. 构建融资约束样本集的文本均值向量(标准化) ConstrainedVec_it
6. 基于前面几个变量,计算得到
- BoardScore_bt 、 InstryScore_it
- 得到5w多个csv文件(中间运算结果), 存储在 fin_constrain_output/{year}/{code}.csv
7. [融资约束FC指标计量建模]
- ConstrainedScore_it =β0 + β1 * BoardScore_bt + β2 * IndustryScore_it + E_it
- BoardScore_bt 交易所引发的融资约束相似度
- IndustryScore_it 行业特征引发的融资约束相似度
- E_it 残差就是本文要计算的[融资约束指标FC]
二、识别融资约束样本
在获取 MD&A 的基础上,采用正则表达式(Regular Expression) 检索出隐含融资约束信息的文本,并把相应的 MD&A 进行标记,纳入对应年度的融资约束文本集中。
为了在 MD&A 文本集中检索出融资约束文本,我们在设计正则表达式时将能显示公司有融资约束的各种文字表达,以词语组合的形式进行提炼。
2.1 融资约束文本的场景
这是一个相对复杂的需求,需要综合考虑多种情况, 对于每种情况,都构建一个单独的正则表达式,用于匹配对应的文本。可以使用“或”运算符, 合并为一个更大的正则表达式。
import re
#融资不足情况
regex1 = r"(?:融资|资金|筹资)[^。]{0,6}?(?:难以|不能|无法|不足以)[^。]*"
#融资成本或压力过大情况
regex2 = r"(?:融资|资金|筹资)[^。]{0,6}?(?:成本|压力|难度)[^。]{0,4}?(?:升|增|高|大)[^。]*"
#可以使用“或”运算符, 合并为一个更大的正则表达式
pattern = r"(" + regex1 + r")|(" + regex2 + r")"
#实验数据
text1 = "公司在过去几年中进行了大量的投资,导致资金短缺,难以支持公司未来的发展计划。"
text2 = "公司在过去几年中进行了大量的投资计划,资金状况良好,没有融资压力。"
#实验结果
matches1 = re.findall(pattern, text1)
print(matches1)
matches2 = re.findall(pattern, text2)
print(matches2)
Run
[('资金短缺,难以支持公司未来的发展计划', '')]
[]
在上面的例子中,pattern能识别出文本是否含有融资约束。
text1有融资约束,所以返回带 有内容 的 matches1 text2没有融资约束,所以返回 没有内容 的 matches2
2.2 识别中文融资约束样本的最终代码
前面的内容都是算法逐步实现的过程,现在咱们合并为一个函数代码
import re
def is_financial_constraint(text):
#正则表达式组
regex1 = r"(?:融资|资金|筹资)[^。]{0,6}?(?:难以|不能|无法|不足以)[^。]*"
regex2 = r"(?:融资|资金|筹资)[^。]{0,6}?(?:成本|压力|难度)[^。]{0,4}?(?:升|增|高|大)[^。]*"
pattern = r"(" + regex1 + r")|(" + regex2 + r")"
#带内容的结果为融资约束,为True;反之,为False
if len(re.findall(pattern, text))>=1:
return True
else:
return False
#实验数据
text1 = "公司在过去几年中进行了大量的投资,导致资金短缺,难以支持公司未来的发展计划。"
text2 = "公司在过去几年中进行了大量的投资计划,资金状况良好,没有融资压力。"
#实验结果
print('text1文本是否为融资约束: ', is_financial_constraint(text1))
print('text2文本是否为融资约束: ', is_financial_constraint(text2))
Run
text1文本是否为融资约束: True
text2文本是否为融资约束: False
三、批量识别融资约束样本
接下来对对 data/mda01-22.csv.gz 数据集所有md&a进行识别。
import pandas as pd
df = pd.read_csv('data/mda01-22.csv.gz', compression='gzip')
df.columns = ['会计年度', '股票代码', '经营讨论与分析内容']
#上市公司行业信息
ind_info_df = pd.read_excel('data/行业代码00-22.xlsx')
#合并数据
df = pd.merge(df, ind_info_df, on=['股票代码', '会计年度'], how='inner')
print(len(df))
df.head()
Run
55767
新建板块字段, 上海证券交易所股票大多以 6、9开头, 而深圳证券交易所以0、3开头
def plate(code):
if (code[:2]=='A6') or (code[:2]=='A9'):
return '上海'
elif (code[:2]=='A0') or (code[:2]=='A3'):
return '深圳'
else:
return '其他'
df['板块'] = df['股票代码'].apply(plate)
df.head()
df['融资约束'] = df['经营讨论与分析内容'].apply(is_financial_constraint)
df.head()
#融资约束样本占比
df['融资约束'].sum()/len(df)
0.1062814926390159
注意
设计的 函数is_financial_constraint 应该要检查, 检查的目的是改良正则表达式组, 这里假装我们检查完了,没什么问题。
四、构建融资约束指标
前面的融资约束样本识别,只是识别出融资约束是否存在,信息的颗粒度比较粗糙。这篇论文使用文本相似度算法,构建了每家企业的融资约束指标。
本文同样参照 Hoberg 和 Maksimovic(2015)的研究方法,我们认为,融资约束程度相近的公司,其在“管理层讨论与分析”中的用词和表述也会趋于一致。 因此,通过采用余弦相似度的方法,能够在识别出全体样本的融资约束程度,并以连续变量的形式进行呈现。
具体实现算法步骤
给每个 md&a 文本转化为向量 Vec_it
当年所有属于融资约束样本的 Vec_it
, 求均值得到ConstrainedVec_t
每家企业当年融资约束水平(程度) 由 Vec_it 与
ConstrainedVec_t之积 , 即
ConstrainedScore_it`` 所体现。考虑到市场板块、行业性因素对融资约束的影响,不能直接使用 ConstrainedScore_it
。
对历年隶属于各个板块的公司 MD&A,求标准化词频向量的均值并做标准化处理,记为 BoardVectb_bt ,该向量反映了上市板 b 在 t 年的共同性信息披露内容。 Vec_it
与对应板块BoardVec_bt
之积,即为因 MD&A 共性内容导致的相似度, 记作BoilerplateScore_i
。利用相同方法,计算出因行业特征引发的相似度,记作 IndustryScore_it
。
ConstrainedScore_it =β0 + β1 * BoardScore_bt + β2 * IndustryScore_it + E_it
- BoardScore_bt 交易所引发的融资约束相似度
- IndustryScore_it 行业特征引发的融资约束相似度
- E_it 残差就是本文要计算的[融资约束指标FC]
4.1 计算2020年的Vec_it
计算量太大,先以2020为例写代码。
df_per_year = df[df['会计年度']==2020]
df_per_year.reset_index(inplace=True)
df_per_year.head()
处理2020年的 「经营讨论与分析内容」字段内容,使其:
只保留中文内容 剔除停用词 整理为用空格间隔的字符串(类西方语言文本格式) 将本文转为向量后,标准化。 合并一些需要的字段,如 ['股票代码', '会计年度', '板块', '行业代码', '融资约束']
%%time
from sklearn.feature_extraction.text import CountVectorizer
import numpy as np
import cntext as ct
import jieba
import re
#cntext1.x
#stopwords = ct.load_pkl_dict('STOPWORDS.pkl')['STOPWORDS']['chinese']
#cntext2.x
stopwords= ct.read_yaml_dict('enzh_common_StopWords.yaml')['Dictionary']['chinese']
def transform(text):
#只保留md&a中的中文内容
text = ''.join(re.findall('[\u4e00-\u9fa5]+', text))
#剔除停用词
words = [w for w in jieba.cut(text) if w not in stopwords]
#整理为用空格间隔的字符串(类西方语言文本格式)
return ' '.join(words)
df_per_year['clean_text'] = df_per_year['经营讨论与分析内容'].apply(transform)
cv = CountVectorizer(min_df=0.05, max_df=0.5)
# 生成稀疏bow矩阵
#dtm 文档-词频-矩阵
dtm_per_year = cv.fit_transform(df_per_year['clean_text'])
dtm_per_year = pd.DataFrame(dtm_per_year.toarray())
#向量标准化normalize
dtm_per_year = dtm_per_year.apply(lambda row: row/np.sum(row), axis=1)
#合并多个字段为新的df
dtm_per_year = pd.concat([df_per_year[['股票代码', '会计年度', '板块', '行业代码', '融资约束']], dtm_per_year], axis=1)
dtm_per_year.head()
Run
CPU times: user 5.88 s, sys: 901 ms, total: 6.78 s
Wall time: 49.7 s
4.2 2020年的板块评分、行业评分
计算2020年所有公司的 板块评分BoardScore、行业评分IndustrySocre。该部分代码运行较慢,运行下来大约2小时。
%%time
import os
import pandas as pd
year = 2020
if not os.path.exists('fin_constrain_output'):
os.mkdir('fin_constrain_output')
for idx in range(len(dtm_per_year)):
code = dtm_per_year.loc[idx, '股票代码']
ind = dtm_per_year.loc[idx, '行业代码']
year = dtm_per_year.loc[idx, '会计年度']
board = dtm_per_year.loc[idx, '板块']
Vec = dtm_per_year.iloc[idx, 5:]
Ind_Vec = dtm_per_year[dtm_per_year['行业代码']==ind][dtm_per_year['股票代码']!=code].iloc[:, 5:].mean(axis=0)
Ind_Score = Vec * (Ind_Vec/np.sum(Ind_Vec))
FinConstrain_Vec = dtm_per_year[dtm_per_year['融资约束']==True].iloc[:, 5:].mean(axis=0)
FinConstrain_Score = Vec * (FinConstrain_Vec/np.sum(FinConstrain_Vec))
Board_Vec = dtm_per_year[dtm_per_year['板块']==board][dtm_per_year['股票代码']!=code].iloc[:, 5:].mean(axis=0)
Board_Score = Vec * (Board_Vec/np.sum(Board_Vec))
dtm_per_year_melted = dtm_per_year.melt(id_vars=['股票代码', '会计年度', '行业代码', '板块', '融资约束'],
var_name='word_id',
value_name='word_freq')
corporate_df = pd.DataFrame({'word_id': dtm_per_year_melted[dtm_per_year_melted['股票代码']==code]['word_id'].values,
'word_freq': dtm_per_year_melted[dtm_per_year_melted['股票代码']==code]['word_freq'].values,
'ind_freq': Ind_Score,
'board_freq': Board_Score,
'fin_constrain_freq': FinConstrain_Score})
corporate_df['股票代码'] = code
corporate_df['行业代码'] = ind
corporate_df['板块'] = board
corporate_df['会计年度'] = year
corporate_df.reset_index(inplace=True)
corporate_df = corporate_df[['股票代码', '行业代码', '会计年度', '板块', 'word_id', 'word_freq', 'ind_freq', 'board_freq', 'fin_constrain_freq']]
if not os.path.exists('fin_constrain_output/{year}'.format(year=year)):
os.mkdir('fin_constrain_output/{year}'.format(year=year))
corporate_df.to_csv('fin_constrain_output/{year}/{code}.csv'.format(year=year, code=code), index=False, mode='w')
4.3 计算所有年份板块评分、行业评分
这部分代码,全部运行下来,耗时 20 小时。
%%time
from sklearn.feature_extraction.text import CountVectorizer
import numpy as np
import pandas as pd
import re
import os
from tqdm import tqdm
import cntext as ct
import jieba
if not os.path.exists('fin_constrain_output'):
os.mkdir('fin_constrain_output')
#cntext1.x
#stopwords = ct.load_pkl_dict('STOPWORDS.pkl')['STOPWORDS']['chinese']
#cntext2.x
stopwords= ct.read_yaml_dict('enzh_common_StopWords.yaml')['Dictionary']['chinese']
def is_financial_constraint(text):
#正则表达式组
regex1 = r"(?:融资|资金|筹资)[^。]{0,6}?(?:难以|不能|无法|不足以)[^。]*"
regex2 = r"(?:融资|资金|筹资)[^。]{0,6}?(?:成本|压力|难度)[^。]{0,4}?(?:升|增|高|大)[^。]*"
pattern = r"(" + regex1 + r")|(" + regex2 + r")"
#带内容的结果为融资约束,为True;反之,为False
if len(re.findall(pattern, text))>=1:
return True
else:
return False
def transform(text):
#只保留md&a中的中文内容
text = ''.join(re.findall('[\u4e00-\u9fa5]+', text))
#剔除停用词
words = [w for w in jieba.cut(text) if w not in stopwords]
#整理为用空格间隔的字符串(类西方语言文本格式)
return ' '.join(words)
def plate(code):
#判断股票是在上海证券交易所还是深圳证券交易所
if (code[:2]=='A6') or (code[:2]=='A9'):
return '上海'
elif (code[:2]=='A0') or (code[:2]=='A3'):
return '深圳'
else:
return '其他'
#读取数据
df = pd.read_csv('data/mda01-22.csv.gz', compression='gzip')
df.columns = ['会计年度', '股票代码', '经营讨论与分析内容']
#上市公司行业信息
ind_info_df = pd.read_excel('data/行业代码00-22.xlsx')
#合并数据
df = pd.merge(df, ind_info_df, on=['股票代码', '会计年度'], how='inner')
df['板块'] = df['股票代码'].apply(plate)
df = df[df['板块'].isin(['上海', '深圳'])]
#识别融资约束
df['融资约束'] = df['经营讨论与分析内容'].apply(is_financial_constraint)
for year in df['会计年度'].unique():
df_per_year = df[df['会计年度']==year]
df_per_year.reset_index(inplace=True)
df_per_year['clean_text'] = df_per_year['经营讨论与分析内容'].apply(transform)
cv = CountVectorizer(min_df=0.05, max_df=0.5)
# 生成稀疏bow矩阵
#dtm 文档-词频-矩阵
dtm_per_year = cv.fit_transform(df_per_year['clean_text'])
dtm_per_year = pd.DataFrame(dtm_per_year.toarray())
#向量标准化normalize
dtm_per_year = dtm_per_year.apply(lambda row: row/np.sum(row), axis=1)
#合并多个字段为新的df
dtm_per_year = pd.concat([df_per_year[['股票代码', '会计年度', '板块', '行业代码', '融资约束']], dtm_per_year], axis=1)
for idx in tqdm(range(len(dtm_per_year)), desc=f'{year}进度'):
code = dtm_per_year.loc[idx, '股票代码']
ind = dtm_per_year.loc[idx, '行业代码']
year = dtm_per_year.loc[idx, '会计年度']
board = dtm_per_year.loc[idx, '板块']
Vec = dtm_per_year.iloc[idx, 5:]
Ind_Vec = dtm_per_year[dtm_per_year['行业代码']==ind][dtm_per_year['股票代码']!=code].iloc[:, 5:].mean(axis=0)
Ind_Score = Vec * (Ind_Vec/np.sum(Ind_Vec))
FinConstrain_Vec = dtm_per_year[dtm_per_year['融资约束']==True].iloc[:, 5:].mean(axis=0)
FinConstrain_Score = Vec * (FinConstrain_Vec/np.sum(FinConstrain_Vec))
Board_Vec = dtm_per_year[dtm_per_year['板块']==board][dtm_per_year['股票代码']!=code].iloc[:, 5:].mean(axis=0)
Board_Score = Vec * (Board_Vec/np.sum(Board_Vec))
dtm_per_year_melted = dtm_per_year.melt(id_vars=['股票代码', '会计年度', '行业代码', '板块', '融资约束'],
var_name='word_id',
value_name='word_freq')
corporate_df = pd.DataFrame({'word_id': dtm_per_year_melted[dtm_per_year_melted['股票代码']==code]['word_id'].values,
'word_freq': dtm_per_year_melted[dtm_per_year_melted['股票代码']==code]['word_freq'].values,
'ind_freq': Ind_Score,
'board_freq': Board_Score,
'fin_constrain_freq': FinConstrain_Score})
corporate_df['股票代码'] = code
corporate_df['行业代码'] = ind
corporate_df['板块'] = board
corporate_df['会计年度'] = year
corporate_df.reset_index(inplace=True)
corporate_df = corporate_df[['股票代码', '行业代码', '会计年度', '板块', 'word_id', 'word_freq', 'ind_freq', 'board_freq', 'fin_constrain_freq']]
if not os.path.exists('fin_constrain_output/{year}'.format(year=year)):
os.mkdir('fin_constrain_output/{year}'.format(year=year))
corporate_df.to_csv('fin_constrain_output/{year}/{code}.csv'.format(year=year, code=code), index=False, mode='w')
4.4 融资约束2020
- ConstrainedScore_it =β0 + β1 * BoardScore_bt + β2 * IndustryScore_it + E_it
- BoardScore_bt 交易所引发的融资约束相似度
- IndustryScore_it 行业特征引发的融资约束相似度
- E_it 残差就是本文要计算的[融资约束指标FC]
import pandas as pd
csv_df = pd.read_csv('fin_constrain_output/2020/A000002.csv')
csv_df.head()
#更改字段名。
csv_df.columns = ['股票代码', '行业代码', '会计年度', '板块', 'word_id', 'Vec', 'IndustryScore', 'BoardScore', 'ConstrainedScore']
csv_df.head()
import statsmodels.formula.api as smf
#因变量ConstrainedScore
#解释变量IndustryScore、 BoardScore
formula = 'ConstrainedScore ~ IndustryScore + BoardScore'
model = smf.ols(formula, data=csv_df)
result = model.fit()
print(result.summary())
Run
OLS Regression Results
==============================================================================
Dep. Variable: ConstrainedScore R-squared: 0.988
Model: OLS Adj. R-squared: 0.988
Method: Least Squares F-statistic: 1.416e+05
Date: Wed, 24 Apr 2024 Prob (F-statistic): 0.00
Time: 11:52:11 Log-Likelihood: 46460.
No. Observations: 3426 AIC: -9.291e+04
Df Residuals: 3423 BIC: -9.290e+04
Df Model: 2
Covariance Type: nonrobust
=================================================================================
coef std err t P>|t| [0.025 0.975]
---------------------------------------------------------------------------------
Intercept 1.048e-08 5.37e-09 1.952 0.051 -4.91e-11 2.1e-08
IndustryScore 0.0791 0.000 250.868 0.000 0.079 0.080
BoardScore 0.8076 0.004 196.675 0.000 0.800 0.816
==============================================================================
Omnibus: 2749.003 Durbin-Watson: 1.974
Prob(Omnibus): 0.000 Jarque-Bera (JB): 21379060.688
Skew: 2.083 Prob(JB): 0.00
Kurtosis: 389.973 Cond. No. 7.71e+05
==============================================================================
Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
[2] The condition number is large, 7.71e+05. This might indicate that there are
strong multicollinearity or other numerical problems.
#融资约束FC
FC = sum(abs(result.resid))
print('2020年 A000002融资约束指标 FC: {}'.format(FC))
2020年 A000002融资约束指标FC: 0.00015749392796709594
4.5 融资约束2001-2022
根据步骤4.4我们成功计算出了2020的融资约束FC指标,现在推广到2001-2022, 并将计算结果存储到 fin_constrain2001-2022.csv, csv 含 code、year、FC 三个字段。
%%time
import glob
import csv
import statsmodels.formula.api as smf
with open('fin_constrain2001-2022.csv', 'w', encoding='utf-8', newline='') as csvf:
fieldnames = ['code', 'year', 'FC']
writer = csv.DictWriter(csvf, fieldnames=fieldnames)
writer.writeheader()
for file in glob.glob('fin_constrain_output/*/*.csv'):
try:
df_ = pd.read_csv(file)
df_.columns = ['股票代码', '行业代码', '会计年度', '板块', 'word_id', 'Vec', 'IndustryScore', 'BoardScore', 'ConstrainedScore']
formula = 'ConstrainedScore ~ IndustryScore + BoardScore'
model = smf.ols(formula, data=df_)
result = model.fit()
FC = sum(abs(result.resid))
data = {
'code': df_['股票代码'].unique()[0],
'year': df_['会计年度'].unique()[0],
'FC': FC
}
writer.writerow(data)
except:
pass
最后查看(欣赏)这个融资约束数据 fin_constrain2001-2022.csv
fc_df = pd.read_csv('fin_constrain2001-2022.csv')
fc_df
获取资料
内容创作不易,整个项目打包 200元, 加微信 372335839, 备注「姓名-学校-专业」。
资料截图, 整个资料文件夹体积高达 11 G。
精选内容
LIST | 社科(经管)可用数据集列表LIST | 文本分析代码列表LIST | 社科(经管)文本挖掘文献列表管理科学学报 | 使用「软余弦相似度」测量业绩说明会「答非所问程度」中国工业经济(更新) | MD&A信息含量指标构建代码实现文献&代码 | 使用Python计算语义品牌评分(Semantic Brand Score)数据集(更新) | 2001-2022年A股上市公司年报&管理层讨论与分析数据集(更新) | 372w政府采购合同公告明细数据(2024.03)数据集 | 人民网政府留言板原始文本(2011-2023.12)数据集 | 人民日报/经济日报/光明日报 等 7 家新闻数据集可视化 | 人民日报语料反映七十年文化演变数据集 | 3571万条专利申请数据集(1985-2022年)数据集 | 专利转让数据集(1985-2021)数据集 | 3394w条豆瓣书评数据集
数据集 | 豆瓣电影影评数据集
数据集 | 使用1000w条豆瓣影评训练Word2Vec代码 | 使用 3571w 专利申请数据集构造面板数据代码 | 使用「新闻数据集」计算 「经济政策不确定性」指数数据集 | 国省市三级gov工作报告文本代码 | 使用「新闻数据」生成概念词频「面板数据」代码 | 使用 3571w 专利申请数据集构造面板数据代码 | 使用gov工作报告生成数字化词频「面板数据」