本节借助公司金融中经典的“投资-现金流关系”问题,综合练习数据整理、变量构造、数据清洗和面板回归。公司金融理论只作必要介绍,重点放在数据处理和实证操作。
在公司金融研究中,一个常见判断是:如果外部融资充分、融资摩擦很小,企业投资主要应由投资机会决定;如果企业投资对内部资金状况特别敏感,则可能说明企业难以顺利从外部获得资金。换句话说,投资-现金流敏感性常被用作讨论融资约束的一条实证线索。
本案例围绕三个问题展开:
- 内部资金(经营现金流)越多的公司,投资强度是否更高?
- 在控制杠杆率、公司规模、公司固定效应和年份固定效应之后,这种“投资-现金流敏感性”是否仍然存在?
- 这个结果能否作为企业可能面临融资约束的证据之一?
要回答这些问题,我们至少需要构造以下几个核心变量:
- 被解释变量:投资强度
- 用资本支出除以滞后一期总资产,刻画相对于公司规模的投资支出。
- 主要解释变量:现金流强度
- 用(净利润 + 折旧摊销)除以滞后一期总资产,作为经营现金流的近似指标。
- 控制变量:杠杆率和公司规模
- 杠杆率用总负债除以总资产;
- 公司规模用总资产的自然对数。
基于这些变量,我们在后面的实证部分将使用如下基准回归模型:
\[
\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}\):误差项。
这里还读入了托宾 Q 数据。托宾 Q 常被用来代理投资机会,但为了先把主线讲清楚,本节正文的基准模型暂时不加入托宾 Q;课后作业会要求同学把它加入模型,观察现金流强度系数是否发生变化。
数据来源: 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
# 导入包并设置绘图主题、色板、字体和输出格式。
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
okabe_ito = ["#0072B2", "#E69F00", "#009E73", "#D55E00", "#CC79A7", "#56B4E9", "#999999"]
sns.set_theme(
style="ticks", # 使用带刻度、少装饰的主题
context="notebook", # 使用适合 notebook 阅读的字号
)
sns.set_palette(okabe_ito) # 使用色盲友好色板
plt.rcParams["font.sans-serif"] = ["Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei"] # 支持中文
plt.rcParams["axes.unicode_minus"] = False # 正常显示负号
%config InlineBackend.figure_formats = ["svg"] # 使用 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 |
样本和变量说明
完成清洗之后,先看一下最终样本的基本情况。实证分析通常需要交代样本包含多少条公司-年份观测、覆盖哪些年份、涉及多少家公司。这样读者才能判断样本范围和后续回归结果的基础。
# 汇总清洗后样本的公司数量、年份范围和观测数量
sample_summary = pd.Series(
{
"公司数量": df_clean["股票代码"].nunique(),
"观测数量": len(df_clean),
"起始年份": int(df_clean["会计年度"].min()),
"结束年份": int(df_clean["会计年度"].max()),
}
)
sample_summary
公司数量 5511
观测数量 62326
起始年份 1998
结束年份 2024
dtype: int64
下面把本案例使用的主要变量整理成一张说明表。变量说明表的作用是把“变量名、定义、经济含义”集中放在一起,方便后面阅读描述性统计和回归结果。
Code
# 整理主要变量的定义和经济含义
variable_info = pd.DataFrame(
{
"变量": ["投资强度", "现金流强度", "杠杆率", "ln_总资产", "托宾Q值A"],
"定义": [
"资本支出 / 滞后一期总资产",
"(净利润 + 折旧摊销)/ 滞后一期总资产",
"总负债 / 总资产",
"总资产的自然对数",
"相对价值指标,作为投资机会的代理变量",
],
"经济含义": [
"衡量企业相对于规模的投资支出",
"衡量企业内部资金的充裕程度",
"衡量企业负债融资程度",
"控制企业规模差异",
"衡量企业面对的投资机会,本节作业中使用",
],
}
)
variable_info
| 0 |
投资强度 |
资本支出 / 滞后一期总资产 |
衡量企业相对于规模的投资支出 |
| 1 |
现金流强度 |
(净利润 + 折旧摊销)/ 滞后一期总资产 |
衡量企业内部资金的充裕程度 |
| 2 |
杠杆率 |
总负债 / 总资产 |
衡量企业负债融资程度 |
| 3 |
ln_总资产 |
总资产的自然对数 |
控制企业规模差异 |
| 4 |
托宾Q值A |
相对价值指标,作为投资机会的代理变量 |
衡量企业面对的投资机会,本节作业中使用 |
在建模之前,先对核心变量做一次描述性统计,了解它们的大致分布和尺度。
- 选取投资强度、现金流强度、杠杆率和规模四个变量;
- 计算均值、标准差、分位数等基本统计量。
# 对核心变量做一次描述性统计,了解它们的大致分布和尺度。
# 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 |
下面通过直方图粗略观察总资产的分布,判断样本中公司规模的集中程度和极端值情况。
这里使用前面 seaborn 章节中的整理方式:先创建 fig 和 ax,再用 seaborn 作图,最后统一调整标题、坐标轴、边框和留白。
# 绘制总资产的直方图,并整理标题、坐标轴和边框
fig, ax = plt.subplots(figsize=(8, 5))
sns.histplot(
data=df_clean,
x='总资产',
bins=30, # 设置直方图分组数量
color=okabe_ito[0],
ax=ax,
)
ax.set_title('总资产分布')
ax.set_xlabel('总资产')
ax.set_ylabel('公司-年份观测数')
sns.despine(ax=ax) # 去掉上边框和右边框
fig.tight_layout() # 自动调整留白
plt.show()
同样地,对取对数后的总资产绘制直方图。和原始总资产相比,对数总资产通常更集中,更便于观察公司规模的大致分布。
# 绘制对数总资产的直方图,并整理图形细节
fig, ax = plt.subplots(figsize=(8, 5))
sns.histplot(
data=df_clean,
x='ln_总资产',
bins=30, # 设置直方图分组数量
color=okabe_ito[0],
ax=ax,
)
ax.set_title('对数总资产分布')
ax.set_xlabel('ln(总资产)')
ax.set_ylabel('公司-年份观测数')
sns.despine(ax=ax)
fig.tight_layout()
plt.show()
为了直观感受投资强度与现金流强度之间的关系,本段代码绘制散点图并叠加一条简单的拟合线。
- 以现金流强度为横轴、投资强度为纵轴;
- 使用较透明的小点表示观测;
- 用同一色板中的强调色显示线性拟合结果。
# 绘制投资强度与现金流强度关系的散点图,并叠加一条简单的拟合线。
fig, ax = plt.subplots(figsize=(8, 5))
sns.regplot(
data=df_clean,
x='现金流强度',
y='投资强度',
scatter_kws={'alpha': 0.2, 's': 10}, # alpha 控制透明度,s 控制点的大小
line_kws={'color': okabe_ito[3], 'linewidth': 2}, # 使用色板中的强调色画拟合线
ax=ax,
)
ax.set_title('投资强度与现金流强度的简单关系')
ax.set_xlabel('现金流强度')
ax.set_ylabel('投资强度')
sns.despine(ax=ax)
fig.tight_layout()
plt.show()
基准回归
在这一部分,我们基于前面构造的变量,估计如下基准回归模型:
\[
\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)。
下面的代码先构造回归所需的数据集,然后依次给出无固定效应、带固定效应的单变量回归,以及包含控制变量的基准回归。这里的基准模型暂时不加入托宾 Q,目的是先练习固定效应模型的基本写法和结果解读;托宾 Q 作为投资机会代理变量,放在课后作业中扩展。
在进入面板回归前,需要从清洗后的数据中提取回归所需的变量,并删除仍有缺失值的观测。
- 选取股票代码、会计年度以及投资强度、现金流强度、杠杆率和规模四个变量;
- 去除行内仍存在缺失值的观测,得到
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: |
Mon, Jun 08 2026 |
R-squared (Overall): |
0.1078 |
| Time: |
11:34:19 |
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: |
Mon, Jun 08 2026 |
R-squared (Overall): |
0.1026 |
| Time: |
11:34:20 |
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: |
Mon, Jun 08 2026 |
R-squared (Overall): |
0.0867 |
| Time: |
11:34:20 |
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: 0x302085a90
回归结果的解读
从上面的回归表可以看到三个模型的一致结论:
- 无固定效应的简单回归中,现金流强度的系数约为 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 函数,或者自行整理为表格,方便横向对比不同模型和不同样本组的估计结果。