查看原文
其他

管理世界 | 使用md&a数据中计算 「企业融资约束指标」

大邓 大邓和他的Python
2024-09-10

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)的研究方法,我们认为,融资约束程度相近的公司,其在“管理层讨论与分析”中的用词和表述也会趋于一致。 因此,通过采用余弦相似度的方法,能够在识别出全体样本的融资约束程度,并以连续变量的形式进行呈现。

具体实现算法步骤

  1. 给每个 md&a 文本转化为向量 Vec_it
  2. 当年所有属于融资约束样本的 Vec_it , 求均值得到 ConstrainedVec_t
  3. 每家企业当年融资约束水平(程度) 由 Vec_it 与 ConstrainedVec_t之积 , 即ConstrainedScore_it`` 所体现。
  4. 考虑到市场板块、行业性因素对融资约束的影响,不能直接使用 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年的 「经营讨论与分析内容」字段内容,使其:

    1. 只保留中文内容
    2. 剔除停用词
    3. 整理为用空格间隔的字符串(类西方语言文本格式)
    4. 将本文转为向量后,标准化。
    5. 合并一些需要的字段,如['股票代码', '会计年度', '板块', '行业代码', '融资约束']
    %%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 含 codeyearFC 三个字段。

    %%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工作报告生成数字化词频「面板数据」


    继续滑动看下一个
    大邓和他的Python
    向上滑动看下一个

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存