本节的主旨不在于系统讲授公司金融理论,而是借助一个经典的“投资–现金流关系”问题,综合练习数据整理、变量构造、数据清洗和面板回归等技术。理论背景只作简要介绍,重点在于“如何动手做”。
在公司金融研究中,一个常见的观点是:如果外部融资完全充分且没有摩擦,企业的投资决策应主要由投资机会决定,而不应过度依赖当期内部资金(经营现金流)。反过来说,如果一个企业的投资支出对内部资金的状况非常敏感,往往被解读为存在一定程度的融资约束。
在本案例中,我们围绕以下问题展开:
- 内部资金(经营现金流)越多的公司,投资强度是否显著更高?
- 在控制了杠杆率和公司规模等因素之后,这种“投资–现金流敏感性”是否仍然存在?
- 这一发现是否可以被解释为企业面临外部融资约束的证据之一(在不深入展开理论细节的前提下)?
要回答这些问题,我们至少需要构造以下几个核心变量:
- 被解释变量:投资强度
- 用资本支出除以滞后一期总资产,刻画相对于公司规模的投资支出。
- 主要解释变量:现金流强度
- 用(净利润 + 折旧摊销)除以滞后一期总资产,作为经营现金流的近似指标。
- 控制变量:杠杆率和公司规模
- 杠杆率用总负债除以总资产;
- 公司规模用总资产的自然对数。
基于这些变量,我们在后面的实证部分将使用如下基准回归模型:
\[
\text{Invest}_{i,t}= \alpha + \beta\, \text{CF}_{i,t} + \gamma\, \text{Lev}_{i,t} + \delta \ln( \text{TA}_{i,t}) + \mu_i + \lambda_t + \varepsilon_{i,t}
\]
其中:
- \(\text{Invest}_{i,t}\):公司 \(i\) 在年份 \(t\) 的投资强度(资本支出除以滞后一期总资产);
- \(\text{CF}_{i,t}\):公司 \(i\) 在年份 \(t\) 的现金流强度(净利润 + 折旧摊销,除以滞后一期总资产);
- \(\text{Lev}_{i,t}\):杠杆率(总负债除以总资产);
- \(\ln( \text{TA}_{i,t})\):公司规模(总资产取自然对数);
- \(\mu_i\):公司固定效应;
- \(\lambda_t\):年份固定效应;
- \(\varepsilon_{i,t}\):误差项。
数据来源: CSMAR(国泰安)数据库。这里已经预先对数据进行了简化,可以直接从以下链接下载(教义默认把数据放在data文件夹下):
- 财务信息.xlsx
- 相对价值.xlsx
- 折旧摊销.xlsx
需要的包和模块: 这里使用了 linearmodels 来进行固定效应回归。如果你尚未安装这个包,或者导入的时候出错,可以在 Notebook 中创建一个 Python 单元格,并在其中执行以下代码,即可从清华大学的软件源镜像中安装这个包:
!pip install -i https://pypi.tuna.tsinghua.edu.cn/simple linearmodels
注意:
- 开头有个感叹号。
- 执行一次,等待完成后即安装成功。测试能否 import,如果成功,则可以删除上述安装代码。
在接下来的内容中,我们要做的事情包括:
- 构造数据:把所需的原始数据表读入,并按公司代码和会计年度进行整合与清洗;
- 初步检查与可视化:查看关键变量的分布和变量之间的简单关系;
- 建模:用一个简单的面板回归模型来刻画投资强度与现金流强度之间的关系,并讨论其含义。
本节涉及的知识
本节结合投资–现金流的实证案例,主要练习以下几个方面:
- Python 基础:在 Jupyter / Quarto 中使用
pandas 和 numpy 处理表格数据,借助 matplotlib 和 seaborn 完成简单绘图;
- 数据处理:多表合并、构造滞后变量和比率变量、对数变换、处理缺失值与无穷值,以及对极端值进行缩尾(winsorization);
- 计量经济学:面板数据的基本形式、公司固定效应和年份固定效应回归,以及如何阅读回归结果表并给出系数的经济解释。
Code
# 本单元格主要做一些“环境配置”,后面所有代码都依赖这些设置。
# pandas 用于表格型数据的读入和处理
import pandas as pd
# numpy 用于数值计算,例如对数、缺失值处理等
import numpy as np
# 导入 matplotlib 的绘图接口,配合 pandas / numpy 画图
import matplotlib.pyplot as plt
# seaborn 在 matplotlib 基础上做了封装,默认图形更美观
import seaborn as sns
# 统一设置 seaborn 的绘图主题
sns.set_theme()
# 设置中文字体,避免图中的中文标签出现乱码。
# 这里依次尝试冬青黑、微软雅黑和文泉驿微米黑,大多数操作系统至少会有其中一个。
plt.rcParams["font.sans-serif"] = ["Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei"]
# 让坐标轴上的负号也能正常显示,而不是方框或乱码
plt.rcParams["axes.unicode_minus"] = False
# 下面这一行是 Jupyter / Quarto 环境中的“魔法命令”(不是普通的 Python 函数调用),
# 用来设置图像输出的格式为 SVG 向量图,放大缩小时都比较清晰。
%config InlineBackend.figure_formats = ['svg']
构造数据
在正式建模之前,需要先明确本案例依赖哪些原始数据表,以及它们各自包含的关键信息,后续会将这些表整理为按“公司–年份”排列的面板数据。
本例中主要使用三类数据:
- 财务信息:总资产、总负债、净利润、资本支出等;
- 相对价值指标:托宾Q值,用于刻画投资机会;
- 折旧和摊销:用于构造现金流强度。
读取数据
接下来用 pandas 从本地 Excel 文件中依次读入这三类数据,并统一股票代码的格式,方便后续按照“股票代码 + 会计年度”进行合并。
接下来的几段代码依次把三张原始 Excel 表读入内存,本单元首先读取包含总资产、总负债、净利润、资本支出等指标的财务信息表,作为后续构造投资和现金流变量的基础。
- 用
read_excel 读取 财务信息.xlsx;
- 将股票代码按字符串类型导入,保证后续合并时代码格式一致;
- 查看尾部几行,检查数据是否正确读取。
# 读取财务信息表:包含总资产、总负债、净利润、资本支出等基础财务指标。
# read_excel 会把 Excel 文件读成一个 DataFrame(类似带表头的电子表格)。
# converters 参数把“股票代码”这一列强制读成字符串,避免像 000001 这样的代码被当成整数而丢掉前导 0。
df_fin = pd.read_excel('data/财务信息.xlsx', converters={'股票代码': str})
# 用 tail() 看看表格最后几行,确认数据是否成功读入以及列名是否正确。
df_fin.tail()
| 76259 |
920992 |
2020 |
5.122577e+08 |
3.336364e+08 |
42591653.33 |
3.722132e+08 |
6934736.35 |
| 76260 |
920992 |
2021 |
5.198337e+08 |
2.780717e+08 |
67493509.40 |
4.652855e+08 |
20420360.97 |
| 76261 |
920992 |
2022 |
8.071261e+08 |
2.059728e+08 |
52586940.63 |
4.064891e+08 |
17328709.42 |
| 76262 |
920992 |
2023 |
7.328239e+08 |
1.313039e+08 |
16126507.82 |
3.027857e+08 |
7168707.21 |
| 76263 |
920992 |
2024 |
7.452764e+08 |
1.335353e+08 |
18087029.57 |
2.957733e+08 |
4677833.96 |
这一段代码读取包含托宾Q值的相对价值表,为后续扩展分析提供投资机会的衡量指标。
- 从
相对价值.xlsx 中读取数据;
- 同样把股票代码作为字符串处理;
- 简单查看尾部数据,确认读入无误。
# 读取相对价值表:主要包含托宾 Q 值等用于刻画投资机会的指标。
# 同样把股票代码读成字符串类型,方便后续按代码合并。
df_q = pd.read_excel('data/相对价值.xlsx', converters={'股票代码': str})
# 查看尾部几行,检查数据结构和列名。
df_q.tail()
| 76704 |
920985 |
2025 |
1.231074 |
| 76705 |
920992 |
2022 |
1.136582 |
| 76706 |
920992 |
2023 |
1.250554 |
| 76707 |
920992 |
2024 |
1.275728 |
| 76708 |
920992 |
2025 |
1.563001 |
这一段代码读取折旧和摊销信息,用于在现金流强度中加入折旧项,使现金流指标更加接近经营现金流。
- 从
折旧摊销.xlsx 中读取数据;
- 股票代码保持为字符串类型;
- 查看尾部几行,确认数据结构。
# 读取折旧和摊销信息:用于在现金流中加入折旧项,使现金流指标更接近经营现金流。
# 股票代码依然保持为字符串类型。
df_dep = pd.read_excel('data/折旧摊销.xlsx', converters={'股票代码': str})
# 查看尾部几行,确认读入无误。
df_dep.tail()
| 94273 |
920992 |
2021 |
8616184.59 |
| 94274 |
920992 |
2022 |
10633218.60 |
| 94275 |
920992 |
2023 |
12777364.33 |
| 94276 |
920992 |
2024 |
13703451.55 |
| 94277 |
920992 |
2025 |
NaN |
数据整合
读入原始表之后,需要按照公司和年份把不同来源的信息合并在一起,形成一张包含投资、现金流、杠杆率等变量的宽表,作为后续变量构造和清洗的基础。
在读入三张表之后,需要按照公司和年份把它们合并在一起,得到一张包含全部变量的宽表。
- 以
股票代码 和 会计年度 为键进行合并;
- 使用内连接保留三张表都有记录的观测;
- 检查合并后的数据结构。
# 将三张表按“股票代码 + 会计年度”这对键合并,得到一张宽表。
# 第一次 merge 把财务数据和托宾 Q 合在一起,第二次 merge 再把折旧摊销信息合进来。
# how='inner' 表示只保留三张表中都存在的公司-年份组合,保证回归时各变量同时有值。
df_all = pd.merge(
df_fin,
df_q,
on=['股票代码', '会计年度'],
how='inner'
).merge(
df_dep,
on=['股票代码', '会计年度'],
how='inner'
)
# 查看合并后数据的尾部,了解行数和列名是否符合预期。
df_all.tail()
| 70191 |
920985 |
2023 |
3.749137e+09 |
2.415459e+09 |
1.344793e+08 |
4.103331e+09 |
3.346390e+08 |
1.164288 |
1.035589e+08 |
| 70192 |
920985 |
2024 |
5.421070e+09 |
3.988678e+09 |
1.683514e+08 |
3.802866e+09 |
6.839254e+08 |
1.133241 |
1.094247e+08 |
| 70193 |
920992 |
2022 |
8.071261e+08 |
2.059728e+08 |
5.258694e+07 |
4.064891e+08 |
1.732871e+07 |
1.136582 |
1.063322e+07 |
| 70194 |
920992 |
2023 |
7.328239e+08 |
1.313039e+08 |
1.612651e+07 |
3.027857e+08 |
7.168707e+06 |
1.250554 |
1.277736e+07 |
| 70195 |
920992 |
2024 |
7.452764e+08 |
1.335353e+08 |
1.808703e+07 |
2.957733e+08 |
4.677834e+06 |
1.275728 |
1.370345e+07 |
为了方便按时间顺序构造滞后变量,本段代码先复制一份合并后的数据,并按公司和年份排序。
- 用
copy 生成工作用的数据框;
- 按
股票代码、会计年度 递增排序;
- 粗略查看排序后的数据。
# 复制一份合并后的数据,作为后面所有处理的起点,避免直接修改原始数据。
df = df_all.copy()
# 为了按时间顺序构造滞后变量,需要先按公司和会计年度排序。
# sort_values 会按照给定的列从小到大排序。
df = df.sort_values(['股票代码', '会计年度'])
# 查看排序后的尾部几行,检查排序是否合理。
df.tail()
| 70191 |
920985 |
2023 |
3.749137e+09 |
2.415459e+09 |
1.344793e+08 |
4.103331e+09 |
3.346390e+08 |
1.164288 |
1.035589e+08 |
| 70192 |
920985 |
2024 |
5.421070e+09 |
3.988678e+09 |
1.683514e+08 |
3.802866e+09 |
6.839254e+08 |
1.133241 |
1.094247e+08 |
| 70193 |
920992 |
2022 |
8.071261e+08 |
2.059728e+08 |
5.258694e+07 |
4.064891e+08 |
1.732871e+07 |
1.136582 |
1.063322e+07 |
| 70194 |
920992 |
2023 |
7.328239e+08 |
1.313039e+08 |
1.612651e+07 |
3.027857e+08 |
7.168707e+06 |
1.250554 |
1.277736e+07 |
| 70195 |
920992 |
2024 |
7.452764e+08 |
1.335353e+08 |
1.808703e+07 |
2.957733e+08 |
4.677834e+06 |
1.275728 |
1.370345e+07 |
在进入变量构造之前,还有一个小检查:
在面板数据的设定下,我们希望“每家公司、每一年”只有一条记录,也就是 (股票代码, 会计年度) 这一对键在数据中是唯一的。如果出现同一家公司同一年有多条观测,往往说明在原始数据或合并过程中存在问题,需要回头核查数据来源或处理步骤。
下面先检查一下合并后的数据中是否存在重复的公司–年份组合。
# 检查 (股票代码, 会计年度) 这一对键是否有重复值。
# 在理想的面板数据中,每家公司每一年应该只有一条记录。
dup_mask = df.duplicated(subset=['股票代码', '会计年度'], keep=False)
dup_count = dup_mask.sum()
print(f"重复的公司-年份组合数量: {dup_count}")
# 如果发现有重复,可以把这些行单独筛出来,回头检查原始数据或合并逻辑。
if dup_count > 0:
df_duplicates = df.loc[dup_mask].sort_values(['股票代码', '会计年度'])
df_duplicates.head()
创建所需变量
有了合并后的财务数据,可以根据经济含义构造投资强度、现金流强度、杠杆率和公司规模等变量,并计算滞后一期总资产,以便将规模因素纳入标准化。
接下来要构造滞后一期的总资产,为定义投资强度和现金流强度做准备。
- 按公司分组,对
总资产 进行按年份排序后的滞后;
- 将结果存入新变量
总资产_lag;
- 抽样查看部分公司近几年的滞后总资产。
# 使用 groupby + shift 构造滞后一期的总资产。
# 思路是:先按股票代码分组,在每个公司内部按年份排序后,
# 用 shift(1) 将“总资产”列向下移动一行,得到上一年的总资产。
df['总资产_lag'] = df.groupby('股票代码')['总资产'].shift(1)
# 抽取部分公司近几年的滞后总资产,简单检查结果是否合理。
df[['股票代码', '会计年度', '总资产_lag']].tail(10)
| 70186 |
920981 |
2023 |
8.141886e+08 |
| 70187 |
920981 |
2024 |
7.671820e+08 |
| 70188 |
920982 |
2023 |
NaN |
| 70189 |
920982 |
2024 |
1.434538e+09 |
| 70190 |
920985 |
2022 |
NaN |
| 70191 |
920985 |
2023 |
3.827076e+09 |
| 70192 |
920985 |
2024 |
3.749137e+09 |
| 70193 |
920992 |
2022 |
NaN |
| 70194 |
920992 |
2023 |
8.071261e+08 |
| 70195 |
920992 |
2024 |
7.328239e+08 |
有了滞后总资产之后,就可以构造投资强度、现金流强度和杠杆率等关键指标。
- 使用资本支出除以滞后总资产得到
投资强度;
- 使用(净利润 + 折旧摊销)除以滞后总资产得到
现金流强度;
- 使用总负债除以总资产得到
杠杆率,并查看这些变量的取值。
# 构造投资强度:资本支出 / 滞后总资产。
# 这样可以将投资规模标准化,便于不同规模公司之间比较。
df['投资强度'] = df['资本支出'] / df['总资产_lag']
# 构造现金流强度:近似的经营现金流 / 滞后总资产。
# 这里用(净利润 + 折旧摊销)作为经营现金流的近似。
df['现金流强度'] = (df['净利润'] + df['折旧摊销']) / df['总资产_lag']
# 构造杠杆率:总负债 / 总资产。
# 用来衡量公司负债融资的程度。
df['杠杆率'] = df['总负债'] / df['总资产']
# 查看几个关键变量的尾部,感受一下大致数值范围。
df[['股票代码', '会计年度', '投资强度', '现金流强度', '杠杆率']].tail(10)
| 70186 |
920981 |
2023 |
0.142039 |
0.042442 |
0.342485 |
| 70187 |
920981 |
2024 |
0.107955 |
0.065650 |
0.376172 |
| 70188 |
920982 |
2023 |
NaN |
NaN |
0.335583 |
| 70189 |
920982 |
2024 |
0.108469 |
0.546793 |
0.255276 |
| 70190 |
920985 |
2022 |
NaN |
NaN |
0.665894 |
| 70191 |
920985 |
2023 |
0.087440 |
0.062198 |
0.644271 |
| 70192 |
920985 |
2024 |
0.182422 |
0.074091 |
0.735773 |
| 70193 |
920992 |
2022 |
NaN |
NaN |
0.255193 |
| 70194 |
920992 |
2023 |
0.008882 |
0.035811 |
0.179175 |
| 70195 |
920992 |
2024 |
0.006383 |
0.043381 |
0.179176 |
这一段代码根据总资产计算公司的规模变量,将其表示为总资产的自然对数。
- 只对总资产为正的观测取对数;
- 将结果存入
ln_总资产,供后续作为控制变量使用。
# 构造规模变量:总资产的自然对数 ln(TotalAssets)。
# 为了避免对 0 或负数取对数,只对总资产为正的观测取 log。
df.loc[df['总资产'] > 0, 'ln_总资产'] = np.log(df.loc[df['总资产'] > 0, '总资产'])
数据清洗
在回归之前,需要先对数据做一些基本清洗,保证关键变量的取值合理、不会因为极端值或异常值影响回归结果。
常见的清洗步骤包括:
- 处理无穷大和缺失值;
- 删除总资产或滞后总资产不合理(如非正数)的观测;
- 对少数极端值进行缩尾 winsor,把最大和最小的一部分观测缩到给定的临界值。
在正式清洗数据之前,先把包含新变量的完整数据框复制一份,作为后续所有处理的起点。
- 保留原始合并结果和派生变量;
- 使用
df_clean 作为后续清洗和回归的工作数据集。
# 在清洗数据之前,先把包含所有派生变量的 df 复制一份。
# 之后的过滤、缩尾等操作都在 df_clean 上进行,不影响原始数据。
df_clean = df.copy()
数据中可能存在无穷大或无穷小的取值,会影响统计描述和回归结果,因此需要先将这些值统一视为缺失。
- 将
inf 和 -inf 替换为缺失值 NaN;
- 为后续的过滤和删除缺失观测做准备。
# 有些变量在计算过程中可能出现无穷大(inf)或无穷小(-inf),
# 比如分母接近 0 的情况。为了后续处理方便,先把这些值统一视为缺失值 NaN。
df_clean = df_clean.replace([np.inf, -np.inf], np.nan)
为了保证比率指标和对数变量的含义合理,本段代码把总资产或滞后总资产不为正的观测剔除掉。
- 要求
总资产 大于 0;
- 要求
总资产_lag 大于 0;
- 得到一组经济含义更清晰的观测样本。
# 为了保证比率和对数变量的经济含义合理,
# 我们只保留总资产和滞后总资产都为正的观测。
if '总资产' in df_clean.columns:
df_clean = df_clean[df_clean['总资产'] > 0]
if '总资产_lag' in df_clean.columns:
df_clean = df_clean[df_clean['总资产_lag'] > 0]
在处理完无穷值和不合理观测之后,最后统一删除仍然存在缺失值的观测,得到用于回归的干净样本。
- 对清洗后的数据调用
dropna;
- 简要查看末尾几行,确认关键变量均无缺失。
# 统一删除仍然存在缺失值的观测,得到用于回归的干净样本。
# dropna 会删除任意一列为 NaN 的行。
df_clean = df_clean.dropna()
# 粗略查看末尾几行,确认关键变量不再有缺失。
df_clean.iloc[-5:, -5:]
| 70189 |
1.434538e+09 |
0.108469 |
0.546793 |
0.255276 |
21.444064 |
| 70191 |
3.827076e+09 |
0.087440 |
0.062198 |
0.644271 |
22.044792 |
| 70192 |
3.749137e+09 |
0.182422 |
0.074091 |
0.735773 |
22.413559 |
| 70194 |
8.071261e+08 |
0.008882 |
0.035811 |
0.179175 |
20.412416 |
| 70195 |
7.328239e+08 |
0.006383 |
0.043381 |
0.179176 |
20.429266 |
在建模之前,先对核心变量做一次描述性统计,了解它们的大致分布和尺度。
- 选取投资强度、现金流强度、杠杆率和规模四个变量;
- 计算均值、标准差、分位数等基本统计量。
# 对核心变量做一次描述性统计,了解它们的大致分布和尺度。
# describe() 会给出均值、标准差、分位数等信息,
# .T 表示转置,方便以“变量为行”的形式阅读结果。
df_clean[['投资强度', '现金流强度', '杠杆率', 'ln_总资产']].describe().round(4).T
| 投资强度 |
62326.0 |
0.0681 |
0.7290 |
-0.1194 |
0.0141 |
0.0373 |
0.0793 |
122.8426 |
| 现金流强度 |
62326.0 |
0.0852 |
1.9577 |
-36.5659 |
0.0317 |
0.0625 |
0.1013 |
314.6799 |
| 杠杆率 |
62326.0 |
0.4887 |
3.6413 |
-0.1947 |
0.2845 |
0.4434 |
0.6013 |
877.2559 |
| ln_总资产 |
62326.0 |
22.0025 |
1.3752 |
12.3143 |
21.0631 |
21.8239 |
22.7470 |
28.7908 |
为了减少少数极端值对回归结果的影响,本段代码对几个连续变量做 1%–99% 的缩尾处理,并在处理后再次查看描述性统计。
- 选择投资强度、现金流强度、杠杆率、总资产和规模作为缩尾对象;
- 使用
winsorize 对两端各 1% 的观测进行裁剪;
- 再次输出描述性统计,观察极端值被压缩后的变化。
# 从 scipy 导入 winsorize 函数,用于对变量进行缩尾(winsorization)。
from scipy.stats.mstats import winsorize
# 选择几个需要缩尾的连续变量:投资强度、现金流强度、杠杆率、总资产和规模。
winsor_cols = ['投资强度', '现金流强度', '杠杆率', '总资产', 'ln_总资产']
# 对每个变量做 1%–99% 的缩尾处理:
# limits=[0.01, 0.01] 表示把最小的 1% 和最大的 1% 的观测值
# 压到对应分位数上,从而减少极端值对回归结果的影响。
for col in winsor_cols:
df_clean[col] = winsorize(df_clean[col], limits=[0.01, 0.01])
# 缩尾之后再看一次描述性统计,观察极端值被压缩后的变化。
df_clean[['投资强度', '现金流强度', '杠杆率', 'ln_总资产']].describe().round(4).T
| 投资强度 |
62326.0 |
0.0593 |
0.0665 |
0.0001 |
0.0141 |
0.0373 |
0.0793 |
0.3547 |
| 现金流强度 |
62326.0 |
0.0637 |
0.0815 |
-0.2453 |
0.0317 |
0.0625 |
0.1013 |
0.3353 |
| 杠杆率 |
62326.0 |
0.4506 |
0.2155 |
0.0584 |
0.2845 |
0.4434 |
0.6013 |
1.0562 |
| ln_总资产 |
62326.0 |
22.0011 |
1.3338 |
19.2895 |
21.0631 |
21.8239 |
22.7470 |
26.1129 |
下面通过直方图粗略观察总资产的分布,判断样本中公司规模的集中程度和极端值情况。
# 绘制总资产的直方图,初步查看公司规模的分布情况。
# bins=30 表示将取值范围分成 30 个区间。
df_clean['总资产'].hist(bins=30)
同样地,对取对数后的总资产绘制直方图,可以更直观地看到规模分布是否近似对称。
- 绘制
ln_总资产 的直方图;
- 与原始总资产的分布进行比较。
# 同样绘制 ln(总资产) 的直方图,看看对数变换后分布是否更加集中、接近对称。
df_clean['ln_总资产'].hist(bins=30)
为了直观感受投资强度与现金流强度之间的关系,本段代码绘制散点图并叠加一条简单的拟合线。
- 以现金流强度为横轴、投资强度为纵轴;
- 使用较透明的小点表示观测;
- 用红色直线表示线性拟合结果。
# 绘制投资强度与现金流强度关系的散点图,并叠加一条简单的拟合线。
# regplot 会在散点的基础上拟合一条线性回归线,帮助我们直观了解变量之间的线性关系。
sns.regplot(
data=df_clean,
x='现金流强度',
y='投资强度',
scatter_kws={'alpha': 0.2, 's': 10}, # alpha 控制透明度,s 控制点的大小
line_kws={'color': 'red'} # 拟合线用红色显示
)
基准回归
在这一部分,我们基于前面构造的变量,估计如下基准回归模型:
\[
\text{Invest}_{i,t}
= \alpha
+ \beta\, \text{CF}_{i,t}
+ \gamma\, \text{Lev}_{i,t}
+ \delta \ln(\text{TA}_{i,t})
+ \mu_i + \lambda_t + \varepsilon_{i,t}.
\]
在本例中:
- 被解释变量
投资强度 是资本支出除以滞后一期总资产;
- 主要解释变量
现金流强度 是(净利润 + 折旧摊销)除以滞后一期总资产;
- 控制变量包括
杠杆率 和 ln_总资产;
- 模型中加入公司固定效应和年份固定效应(
PanelOLS 中的 entity_effects=True, time_effects=True)。
下面的代码先构造回归所需的数据集,然后依次给出无固定效应、带固定效应的单变量回归,以及包含控制变量的基准回归。
在进入面板回归前,需要从清洗后的数据中提取回归所需的变量,并删除仍有缺失值的观测。
- 选取股票代码、会计年度以及投资强度、现金流强度、杠杆率和规模四个变量;
- 去除行内仍存在缺失值的观测,得到
df_reg_basic。
# 为面板回归准备数据集:挑选出需要用到的变量。
# 这里保留股票代码、会计年度,以及投资强度、现金流强度、杠杆率和规模四个变量。
reg_cols_basic = ['股票代码', '会计年度', '投资强度', '现金流强度', '杠杆率', 'ln_总资产']
# 只保留这些变量都不缺失的观测,得到回归用的数据集 df_reg_basic。
df_reg_basic = df_clean[reg_cols_basic].dropna()
df_reg_basic.head()
| 16 |
000001 |
2009 |
0.002077 |
0.011430 |
0.965177 |
26.112864 |
| 36 |
000002 |
1998 |
0.018404 |
0.059707 |
0.479479 |
22.119010 |
| 37 |
000002 |
1999 |
0.019791 |
0.067200 |
0.522476 |
22.226157 |
| 38 |
000002 |
2000 |
0.014025 |
0.073003 |
0.472516 |
22.449997 |
| 39 |
000002 |
2001 |
0.005365 |
0.099183 |
0.517797 |
22.592436 |
下面开始设置面板回归所需的索引和工具函数。
- 导入
statsmodels 和 PanelOLS 等回归函数;
- 将回归数据按
股票代码 和 会计年度 设为多重索引,并按索引排序;
- 为后续估计提供标准的面板数据结构。
# 本段代码开始正式设置面板回归模型。
# 再次导入需要用到的库:
# - pandas 主要用于数据结构;
# - statsmodels.api 提供了一些通用统计工具;
# - PanelOLS 是 linearmodels 库中专门用于面板数据回归的类。
import pandas as pd
import statsmodels.api as sm
from linearmodels.panel import PanelOLS
# ========= 1. 设置面板索引 =========
# PanelOLS 要求数据的索引是“多重索引”,
# 这里用 (股票代码, 会计年度) 这对变量作为索引,表示公司 i 在年份 t 的观测。
df_reg_basic = df_reg_basic.set_index(['股票代码', '会计年度']).sort_index()
df_reg_basic.tail()
| 股票代码 |
会计年度 |
|
|
|
|
| 920982 |
2024 |
0.108469 |
0.335282 |
0.255276 |
21.444064 |
| 920985 |
2023 |
0.087440 |
0.062198 |
0.644271 |
22.044792 |
| 2024 |
0.182422 |
0.074091 |
0.735773 |
22.413559 |
| 920992 |
2023 |
0.008882 |
0.035811 |
0.179175 |
20.412416 |
| 2024 |
0.006383 |
0.043381 |
0.179176 |
20.429266 |
在准备好面板索引之后,本段代码构造被解释变量和解释变量,并给出不含固定效应的基准回归结果。
- 令投资强度为被解释变量
y;
- 分别构造只含现金流强度的单变量自变量集,以及加入杠杆率和规模的多变量自变量集;
- 使用
PanelOLS 在不含固定效应的设定下估计模型,得到基准系数。
# ========= 准备被解释变量和解释变量 =========
# 被解释变量 y:投资强度。
y = df_reg_basic['投资强度']
# 单变量自变量集:只包含现金流强度,用于最简单的回归。
X_single = df_reg_basic[['现金流强度']]
# 基础回归的自变量集:包含现金流强度、杠杆率和规模。
X_basic = df_reg_basic[['现金流强度', '杠杆率', 'ln_总资产']]
# PanelOLS 不会自动添加截距项(常数项),
# 因此需要使用 statsmodels 的 add_constant 函数人为添加一列常数 1。
X_single = sm.add_constant(X_single)
X_basic = sm.add_constant(X_basic)
# ========= 单变量回归(无固定效应) =========
# 首先在不考虑公司和年份固定效应的条件下,估计一个简单的面板回归。
mod_single = PanelOLS(
dependent=y,
exog=X_single,
entity_effects=False,
time_effects=False
)
# 调用 fit() 进行估计,res_single 中包含系数、标准误等结果。
res_single = mod_single.fit()
# 在 Jupyter / Quarto 中,直接输出 summary 可以查看详细回归结果。
res_single.summary
PanelOLS Estimation Summary
| Dep. Variable: |
投资强度 |
R-squared: |
0.1078 |
| Estimator: |
PanelOLS |
R-squared (Between): |
0.0870 |
| No. Observations: |
62326 |
R-squared (Within): |
0.0823 |
| Date: |
Wed, Dec 10 2025 |
R-squared (Overall): |
0.1078 |
| Time: |
16:53:05 |
Log-likelihood |
8.403e+04 |
| Cov. Estimator: |
Unadjusted |
|
|
|
|
F-statistic: |
7531.9 |
| Entities: |
5511 |
P-value |
0.0000 |
| Avg Obs: |
11.309 |
Distribution: |
F(1,62324) |
| Min Obs: |
1.0000 |
|
|
| Max Obs: |
27.000 |
F-statistic (robust): |
7531.9 |
|
|
P-value |
0.0000 |
| Time periods: |
27 |
Distribution: |
F(1,62324) |
| Avg Obs: |
2308.4 |
|
|
| Min Obs: |
513.00 |
|
|
| Max Obs: |
5174.0 |
|
|
|
|
|
|
Parameter Estimates
|
Parameter |
Std. Err. |
T-stat |
P-value |
Lower CI |
Upper CI |
| const |
0.0422 |
0.0003 |
132.22 |
0.0000 |
0.0416 |
0.0429 |
| 现金流强度 |
0.2680 |
0.0031 |
86.786 |
0.0000 |
0.2619 |
0.2740 |
接下来在同样的单变量设定下,加入公司和年份固定效应,考察投资强度与现金流强度在控制固定效应后的关系。
- 在
PanelOLS 中启用实体和时间固定效应;
- 采用按公司聚类的协方差估计;
- 得到带固定效应的单变量回归结果。
# ========= 单变量回归(有固定效应) =========
# 在单变量设定下,加入公司固定效应和年份固定效应,
# 用来控制不同公司之间不随时间变化的特征,以及各年份的宏观冲击。
mod_single_fe = PanelOLS(
dependent=y,
exog=X_single,
entity_effects=True, # entity_effects=True 表示加入公司固定效应
time_effects=True # time_effects=True 表示加入年份固定效应
)
# 使用按公司聚类的协方差矩阵(cluster_entity=True),
# 可以在存在公司层面相关性的情况下得到更稳健的标准误。
reg_single_fe = mod_single_fe.fit(cov_type='clustered', cluster_entity=True)
reg_single_fe.summary
PanelOLS Estimation Summary
| Dep. Variable: |
投资强度 |
R-squared: |
0.0666 |
| Estimator: |
PanelOLS |
R-squared (Between): |
0.0817 |
| No. Observations: |
62326 |
R-squared (Within): |
0.0825 |
| Date: |
Wed, Dec 10 2025 |
R-squared (Overall): |
0.1026 |
| Time: |
16:53:05 |
Log-likelihood |
9.731e+04 |
| Cov. Estimator: |
Clustered |
|
|
|
|
F-statistic: |
4052.2 |
| Entities: |
5511 |
P-value |
0.0000 |
| Avg Obs: |
11.309 |
Distribution: |
F(1,56788) |
| Min Obs: |
1.0000 |
|
|
| Max Obs: |
27.000 |
F-statistic (robust): |
1138.5 |
|
|
P-value |
0.0000 |
| Time periods: |
27 |
Distribution: |
F(1,56788) |
| Avg Obs: |
2308.4 |
|
|
| Min Obs: |
513.00 |
|
|
| Max Obs: |
5174.0 |
|
|
|
|
|
|
Parameter Estimates
|
Parameter |
Std. Err. |
T-stat |
P-value |
Lower CI |
Upper CI |
| const |
0.0460 |
0.0004 |
116.33 |
0.0000 |
0.0452 |
0.0468 |
| 现金流强度 |
0.2093 |
0.0062 |
33.742 |
0.0000 |
0.1971 |
0.2214 |
F-test for Poolability: 5.4497
P-value: 0.0000
Distribution: F(5536,56788)
Included effects: Entity, Time
最后在加入杠杆率和规模等控制变量的条件下,同样估计带公司和年份固定效应的基准回归模型。
- 使用多变量自变量集
X_basic;
- 在公司和年份固定效应下估计模型;
- 获得与前两种设定可比的基准回归结果。
# ========= 基础回归(公司 + 年份固定效应) =========
# 在加入杠杆率和规模等控制变量的基础上,同样估计带公司和年份固定效应的回归模型。
mod_basic_fe = PanelOLS(
dependent=y,
exog=X_basic,
entity_effects=True, # 公司固定效应,控制不随时间变化的公司特征
time_effects=True # 年份固定效应,控制每年共同的宏观影响
)
# 依然采用按公司聚类的协方差矩阵。
res_basic_fe = mod_basic_fe.fit(cov_type='clustered', cluster_entity=True)
res_basic_fe.summary
PanelOLS Estimation Summary
| Dep. Variable: |
投资强度 |
R-squared: |
0.0717 |
| Estimator: |
PanelOLS |
R-squared (Between): |
0.0767 |
| No. Observations: |
62326 |
R-squared (Within): |
0.0537 |
| Date: |
Wed, Dec 10 2025 |
R-squared (Overall): |
0.0867 |
| Time: |
16:53:05 |
Log-likelihood |
9.748e+04 |
| Cov. Estimator: |
Clustered |
|
|
|
|
F-statistic: |
1462.0 |
| Entities: |
5511 |
P-value |
0.0000 |
| Avg Obs: |
11.309 |
Distribution: |
F(3,56786) |
| Min Obs: |
1.0000 |
|
|
| Max Obs: |
27.000 |
F-statistic (robust): |
389.73 |
|
|
P-value |
0.0000 |
| Time periods: |
27 |
Distribution: |
F(3,56786) |
| Avg Obs: |
2308.4 |
|
|
| Min Obs: |
513.00 |
|
|
| Max Obs: |
5174.0 |
|
|
|
|
|
|
Parameter Estimates
|
Parameter |
Std. Err. |
T-stat |
P-value |
Lower CI |
Upper CI |
| const |
-0.0871 |
0.0170 |
-5.1377 |
0.0000 |
-0.1204 |
-0.0539 |
| 现金流强度 |
0.2125 |
0.0065 |
32.605 |
0.0000 |
0.1997 |
0.2253 |
| 杠杆率 |
0.0117 |
0.0032 |
3.6328 |
0.0003 |
0.0054 |
0.0180 |
| ln_总资产 |
0.0058 |
0.0008 |
7.4414 |
0.0000 |
0.0043 |
0.0073 |
F-test for Poolability: 5.4572
P-value: 0.0000
Distribution: F(5536,56786)
Included effects: Entity, Time
为了方便对比不同模型设定下的结果,本段代码将三组回归结果整理到同一张表中。
- 使用
compare 函数汇总无固定效应、单变量固定效应和多变量固定效应三类回归;
- 通过星号标注显著性水平,便于横向比较。
# 导入 compare 函数,用于把多组回归结果整理在一起比较。
from linearmodels.panel.results import compare
# 将三个回归结果放在同一张表中:
# (1) 简单 pooled 模型;
# (2) 含固定效应的单变量模型;
# (3) 含固定效应和控制变量的基准模型。
# stars=True 会按显著性水平自动添加星号标记。
compare(results={'(1)': res_single, '(2)': reg_single_fe, '(3)': res_basic_fe}, stars=True)
Model Comparison
|
(1) |
(2) |
(3) |
| Dep. Variable |
投资强度 |
投资强度 |
投资强度 |
| Estimator |
PanelOLS |
PanelOLS |
PanelOLS |
| No. Observations |
62326 |
62326 |
62326 |
| Cov. Est. |
Unadjusted |
Clustered |
Clustered |
| R-squared |
0.1078 |
0.0666 |
0.0717 |
| R-Squared (Within) |
0.0823 |
0.0825 |
0.0537 |
| R-Squared (Between) |
0.0870 |
0.0817 |
0.0767 |
| R-Squared (Overall) |
0.1078 |
0.1026 |
0.0867 |
| F-statistic |
7531.9 |
4052.2 |
1462.0 |
| P-value (F-stat) |
0.0000 |
0.0000 |
0.0000 |
| ===================== |
============ |
=========== |
============ |
| const |
0.0422*** |
0.0460*** |
-0.0871*** |
|
(132.22) |
(116.33) |
(-5.1377) |
| 现金流强度 |
0.2680*** |
0.2093*** |
0.2125*** |
|
(86.786) |
(33.742) |
(32.605) |
| 杠杆率 |
|
|
0.0117*** |
|
|
|
(3.6328) |
| ln_总资产 |
|
|
0.0058*** |
|
|
|
(7.4414) |
| ======================= |
============== |
============= |
============== |
| Effects |
|
Entity |
Entity |
|
|
Time |
Time |
T-stats reported in parentheses
id: 0x32bb9d9a0
回归结果的解读
从上面的回归表可以大致看到三个模型的一致结论:
- 无固定效应的简单回归中,现金流强度的系数约为 0.27,且在常用显著性水平下高度显著;
- 加入公司和年份固定效应后,单变量回归中现金流强度的系数约为 0.21,t 统计量仍然很大(绝对值在 30 左右),说明在控制个体和时间不变因素后,这一关系依然稳健;
- 在同时控制杠杆率和公司规模的基准回归中,现金流强度的系数依然在 0.21 左右,显著性几乎没有下降,说明这一关系并非完全由简单的规模或杠杆差异驱动。
从技术角度看,可以做出几条总结:
- 三个模型中现金流强度的系数都是正的且高度显著,表明样本中投资强度与内部资金状况之间存在稳定的正相关关系;
- 加入公司和年份固定效应后,系数数值略有下降,但仍然为正且显著,这意味着在控制了公司不变特征和宏观年份冲击之后,企业内部资金越充裕、投资越积极的现象依然存在;
- 在固定效应模型下加入杠杆率和规模等控制变量,对现金流强度系数的影响不大,表明该结果对常见控制变量较为稳健;
- 模型的 R^2 虽然不算很高,但这在以公司投资为被解释变量的实证研究中是常见现象,说明投资决策受到许多难以观测的因素影响。
从经济含义上,可以有以下直观理解:
- 系数约为 0.21–0.27 意味着,在其他条件大致不变的情况下,如果一家公司的现金流强度提高 0.10(例如从 0.05 上升到 0.15),其投资强度平均会提高约 0.021–0.027,这表明内部资金状况对投资有较为明显的影响;
- 即便在控制公司固定效应、年份固定效应以及杠杆率和规模之后,这种“投资–现金流敏感性”仍然显著存在,可以被解读为企业在现实中面临一定程度的外部融资约束:当内部资金较为紧张时,投资支出往往会被压缩;
- 当然,投资–现金流敏感性本身并不能完全证明融资约束的存在,但结合理论背景和其他变量(例如托宾Q、规模分组异质性分析等),可以作为判断融资约束的重要实证线索之一。
在课后作业中,你可以在此基础上进一步加入托宾Q和规模分组,考察在不同投资机会、不同公司规模下,这种投资–现金流敏感性是否会发生改变。
课后作业:扩展投资–现金流关系分析
本小节的代码已经构造了投资强度、现金流强度、杠杆率、规模等变量,并完成了基础的面板回归。请在此基础上,独立完成以下两个扩展练习,并整理为一个可运行的 notebook:
- 将托宾Q加入控制变量
- 数据中已经合并了
托宾Q值A,请将其作为投资机会的代理变量加入到基础回归模型中。
- 要求:
- 在
df_clean 的基础上,构造包含 托宾Q值A 的回归数据集(注意处理缺失值和极端值,可以与其他连续变量采用相同的 winsor 处理方式)。
- 在公司固定效应 + 年份固定效应的设定下,估计以下模型:
- 投资强度 ~ 现金流强度 + 杠杆率 + ln(总资产) + 托宾Q值A
- 比较加入托宾Q前后,现金流强度系数的大小和显著性是否发生变化,并用简短文字解释你的观察。
- 按规模分组的异质性检验
- 利用
ln_总资产 作为公司规模的度量,按照样本内中位数将公司划分为“规模较小”(Small)和“规模较大”(Big)两组。
- 要求:
- 给出分组方法的具体实现(例如:基于某一年整体样本的中位数,或者基于全样本的中位数),并在代码中清晰标注。
- 分别在 Small 组和 Big 组样本上,估计与课堂中相同设定的回归模型(至少包括:
- 投资强度 ~ 现金流强度 + 杠杆率 + ln(总资产)
- 公司固定效应 + 年份固定效应)。
- 对比两组样本中现金流强度系数的大小和显著性,讨论投资–现金流敏感性是否存在“规模异质性”。
提示:
- 建议在每一步回归前,先输出关键变量的描述性统计和相关性矩阵,检查数据是否合理。
- 在汇总回归结果时,可以使用
compare 函数,或者自行整理为表格,方便横向对比不同模型和不同样本组的估计结果。