1991-2024 A股现金流面板清洗与净现比收现比实战
2026/9/17 15:50:10 网站建设 项目流程

简介:本资源为1991年至2024年6月沪深北上市公司财务指标现金流分析数据包,面向金融研究、量化投资、会计实证与论文写作人群,可解决长周期现金流指标获取难、跨软件格式不兼容的问题。压缩包内共1个docx文件,约51KB,以文档形式给出百度网盘数据链接,配套Excel与dta两种数据文件及字段说明,便于SPSS、Stata、Python等不同工具直接读取分析。数据为季度频率,覆盖全部A股主板、中小企业板、创业板、科创板公司,字段涵盖净利润现金净含量、营业收入现金含量、营业利润现金净含量及其TTM值,还包括筹资活动债权人及股东现金净流量、折旧摊销、公司现金流、股权现金流、企业自由现金流、全部现金回收率、营运指数、现金适合比率、现金再投资比率、资本支出与折旧摊销比等派生指标,并按行业代码与行业名称分类。目前已有144人学习,适合做现金流质量、盈余含金量与自由现金流相关实证研究。

1. 上市公司现金流分析数据集为什么值得从1991年一路拉到2024年6月

做基本面量化的人迟早会撞上一份名字很长的压缩包——上市公司财务指标现金流分析1991-202406的网盘链接.docx。它本质是一份跨越三十多年的A股财务长面板,终点是2024年6月30日的半年报,起点是1991年那批数量极少、口径原始的早期样本。很多人拿到它第一反应是直接跑因子,结果被前导零丢掉的股票代码、按万元和元混着写的报表、以及1998年以前大面积为空的现金流字段绊住。

真正值钱的地方在于现金流的连续性。净利润可以靠会计估计调整,营业收入可以靠赊销堆出来,但经营活动产生的现金流量净额要和银行流水对得上,长期偏离的样本几乎都会在后续年份暴雷。把1991到2024年6月这条线拉全,能做的事包括:验证净现比在长周期上的均值回复、观察不同行业收现比的季节性、用现金流正负号组合判断企业所处的生命周期阶段。

适合谁看:做多因子选股的研究员、做信用风险预警的风控工程师、以及需要给财务舞弊模型准备特征的数据开发。前提是你愿意花半天时间把这份数据从网盘落地成本地可信表,而不是把它当成开箱即用的CSV。

2. 从「网盘链接.docx」到本地可信数据:下载件校验与字段落地

网盘分发的数据包有个共同特征:真正的地址藏在一个docx里,正文只有两三行字,链接、提取码、解压密码、甚至校验值混在一起。直接复制粘贴经常漏掉尾部字符,或者把中文全角括号带进URL。正确姿势是用脚本把文档内容整体抽出来,再做正则匹配,而不是肉眼抠。

2.1 用 python-docx 提取文档里的下载地址与校验信息

发布方习惯把链接放在段落里,偶尔也会塞进表格甚至文本框。python-docx 默认只能拿到段落和表格,文本框内容需要走底层XML。下面这段把三种情况都覆盖了。

from docx import Document import re path = "上市公司财务指标现金流分析1991-202406的网盘链接.docx" doc = Document(path) # 1) 普通段落 text = "\n".join(p.text for p in doc.paragraphs) # 2) 表格单元格 for tbl in doc.tables: for row in tbl.rows: for cell in row.cells: text += "\n" + cell.text # 3) 文本框(w:txbxContent),python-docx 不直接暴露 from docx.oxml.ns import qn for node in doc.element.body.iter(qn("w:txbxContent")): for t in node.iter(qn("w:t")): text += "\n" + (t.text or "") # 链接里常见全角括号、中文标点,正则里要排除 urls = re.findall(r"https?://[^\s,,、))】\]]+", text) codes = re.findall(r"(?:提取码|密码|访问码|解压码)[::\s]*([0-9A-Za-z]{4,12})", text) print("URL:", urls) print("CODE:", sorted(set(codes)))

逻辑说明:先把文档里所有可能承载文字的XML节点摊平成一个字符串,再做无状态正则。[^\s,,、))】\]]+这个字符集是关键,网盘链接常以全角右括号结尾,如果只排除空格,提取出来的URL会多带一个字符,请求时直接404。提取码用{4,12}而不是固定长度,因为不同平台的编码长度不一样。参数上没什么可调的,唯一要注意的是doc.element.body只覆盖正文,如果文档里有页眉页脚,需要额外遍历doc.sections

注意:不要用text.split("链接")这类基于位置的解析。同一批数据被不同人转发几次,措辞就会变,只有正则能扛住。

2.2 下载件的落地校验:先看四件事再入库

文件到手后别急着pd.read_csv。先按顺序确认四件事,能把后续80%的脏数据问题挡在门外。

校验项期望结果失败时的动作
文件完整性哈希与发布方 manifest 一致,或解压无报错重新下载,不要尝试修复压缩包
目录结构按年份或按表拆分,命名规则统一先写一份文件清单,再决定合并策略
记录数与主键主键组合唯一,无重复行定位重复来源,多半是合并报表与母公司报表混放
时间跨度最早报告期落在1991-1993,最晚为2024-06-30缺年份说明分包不全,缺末端说明版本滞后
# 发布方给了校验文件就直接验,比肉眼比对靠谱 sha256sum -c manifest.sha256 # 先列目录再解压,避免解压出一堆嵌套文件夹 unzip -l cashflow_pkg.zip | head -50 # 判断编码与BOM,file 对压缩格式和中文编码的识别很直接 file data/*.csv

命令含义:sha256sum -c会逐行读取 manifest 并比对,任何一行 FAILED 都说明文件在传输中被改动过,尤其是分卷压缩的包。unzip -l只列不解压,能提前发现压缩包里套了一层同名目录,直接解压会让你多出一级路径。file输出的UTF-8 Unicode (with BOM)决定了后面 pandas 该用utf-8还是utf-8-sig,用错的话第一列列名会带一个不可见字符,之后所有df["股票代码"]都会抛 KeyError。

2.3 字段落地的三个高频坑:编码、前导零、数值单位

这一步处理完,数据才算能进分析管道。

import pandas as pd df = pd.read_csv( "data/cashflow_1991_202406.csv", encoding="utf-8-sig", # 吃掉BOM dtype={"股票代码": "string", "交易市场": "string"}, # 防止 000001 变成 1 parse_dates=["报表日期", "公告日期"], na_values=["", "-", "--", "NULL", "null", "nan", "N/A"], ) # 补齐到6位,兼容早期只有5位甚至4位的代码 df["股票代码"] = df["股票代码"].str.strip().str.zfill(6) # 单位统一:常见做法是元、千元、万元三种混用,按文件名或字段后缀判定 UNIT = {"元": 1.0, "千元": 1e3, "万元": 1e4} money_cols = [c for c in df.columns if c.endswith(("_元", "_万元", "_千元"))] for c in money_cols: suffix = c.rsplit("_", 1)[-1] df[c] = pd.to_numeric(df[c], errors="coerce") * UNIT[suffix] df = df.rename(columns={c: c.rsplit("_", 1)[0] for c in money_cols})

逻辑与参数:dtype="string"而不是默认的 object,是为了让.str访问器始终可用,同时避免 pandas 把列推断成整数后抹掉前导零。errors="coerce"把无法解析的文本转成 NaN 而不是抛异常,因为这类长面板里一定混着"--"、空格、全角数字。单位统一必须显式做,把万元当元用会让净现比看起来正常但绝对值差一万倍,而净现比是比值,它反而不报错,这是最阴的一类错误。

2.4 1991-2024年6月 的口径断层:别把空值当零

跨度三十多年,最大的障碍不是数据量,是会计准则和披露要求的变更。三个断层必须记住。

1998年之前,上市公司披露的是财务状况变动表,以营运资金为基础,不是现金流量表。所以1991到1997年的样本,经营活动产生的现金流量净额这类字段大面积为空。处理方式是在面板上打一个report_std标志字段,把1998年以前的记录标为pre_cf,计算现金流类指标时直接排除,绝对不要 fillna(0)。填零会让早期样本的净现比全部变成0或无穷,任何按年份做的统计都会失真。

1998到2006年执行旧准则下的现金流量表,2007年起执行新准则,报表项目名称和归集方式有变化,比如"购建固定资产、无形资产和其他长期资产支付的现金"在新准则下才稳定存在。做法是建一张字段映射表,把不同年份的项目名映射到统一的标准列名,映射不上的记入unmapped列,而不是静默丢弃。

第三个断层是金融业。银行、保险、券商没有"销售商品、提供劳务收到的现金",它们用"收取利息、手续费及佣金的现金"。如果全市场统一算收现比,金融业的分子会全是空值,分位数基准被拉偏。正确做法是在行业分类上先把金融业拆出来单独建基准,后面第4章会展开。早期还有一批已退市公司,代码在1991-2005年有记录,之后断档,做时序连续性校验时要把退市标志一起带出来,否则会误判为数据缺失。

3. 财务指标面板的现金流派生:净现比、收现比、自由现金流的计算

原始表只有报表项目,没有指标。净现比、收现比、自由现金流这些要自己派生。派生之前先定键,键定错了后面全白算。

3.1 三张表拼长面板:键、粒度与去重

A股财务数据的主键是(股票代码, 报告期, 报表类型)。报表类型必须区分合并报表和母公司报表,做集团整体分析一律取合并报表,混用会让同一报告期出现两行,指标直接翻倍。报告期粒度上,现金流量表项目是累计值,2024-06-30 那一行代表的是1月到6月的累计数,不是单季数。这一点决定了滚动指标怎么写。

# 只保留合并报表,且只保留标准报告期 df = df[df["报表类型"] == "合并报表"].copy() df = df[df["报表日期"].dt.strftime("%m-%d").isin(["03-31", "06-30", "09-30", "12-31"])] # 主键去重:同一 (代码, 报告期) 保留最新公告的那条 df = (df.sort_values("公告日期") .drop_duplicates(subset=["股票代码", "报表日期"], keep="last") .reset_index(drop=True))

逻辑说明:先按报告期过滤,是因为有些数据源会带上"招股说明书"期或非标报告期,那些行的口径不可比。去重时保留最新公告的一条,处理的是财务重述场景——同一份年报可能因为会计差错更正被重新披露,用最早的版本会拿到已被推翻的数字。

3.2 净现比与收现比的 Python 实现

import numpy as np # 净现比:分母为负时比值失去意义,直接置空 df["净现比"] = np.where( df["净利润"] > 0, df["经营活动产生的现金流量净额"] / df["净利润"], np.nan ) # 收现比:分母用含税收入更合理,增值税率按主体适用税率给 VAT = 1.13 df["收现比"] = np.where( df["营业收入"] > 0, df["销售商品、提供劳务收到的现金"] / (df["营业收入"] * VAT), np.nan ) # 现金流量利息保障倍数 df["现金利息保障倍数"] = np.where( df["现金利息支出"] > 0, (df["经营活动产生的现金流量净额"] + df["现金利息支出"] + df["所得税付现"]) / df["现金利息支出"], np.nan )

参数说明:净现比的分母用净利润而不是归母净利润,是因为经营活动现金流是整个集团口径,与归母净利润不匹配。收现比的分母乘1.13是常见处理,因为现金流量表里的销售收现包含增值税销项,用不含税的营业收入做分母会让健康的制造业公司收现比看起来只有0.88,误判为回款差。np.where的第二个分支返回 NaN 而不是跳过,保证结果列的行数与原表严格对齐。行业差异要注意:农林牧渔、建筑装饰的收现比天然偏低,医药和白酒天然偏高,跨行业比绝对值没有意义,必须用行业内分位。

指标公式健康区间(制造业)常见误用
净现比经营现金流净额 / 净利润0.8 - 1.3,长期 >1净利润为负时不置空
收现比销售收现 / 含税营业收入0.95 - 1.15分母用不含税收入
自由现金流经营现金流净额 − 资本开支长期 >0资本开支漏掉无形资产
现金利息保障倍数(经营现金流+利息+税付现) / 利息>3 相对安全分母用财务费用代替现金利息

3.3 自由现金流与资本开支口径

自由现金流最容易出错的地方在资本开支的取数。现金流量表投资活动下有三个相邻科目:"购建固定资产、无形资产和其他长期资产支付的现金"、"取得子公司及其他营业单位支付的现金净额"、"投资支付的现金"。只有第一个算维持性和扩张性资本开支,后两个属于对外投资,混进来会让重资产公司的自由现金流被严重低估。

df["资本开支"] = df["购建固定资产、无形资产和其他长期资产支付的现金"].fillna(0) df["自由现金流"] = df["经营活动产生的现金流量净额"] - df["资本开支"] df["FCF利润率"] = np.where(df["营业收入"] > 0, df["自由现金流"] / df["营业收入"], np.nan)

逻辑与参数:资本开支这里用了fillna(0),和前面现金流字段的处理看起来矛盾。区别在于,pre_cf时期整张现金流量表都缺,而2007年以后个别公司某个明细科目为空,多数是当年确实没有这笔支出,填零比置空更接近真实。判定规则是:整行关键字段全空按缺失处理,单科目为空按零处理。FCF利润率用营业收入做分母,是为了在规模差异极大的样本间可比,用总资产做分母会被杠杆结构干扰。

3.4 用 SQL 在千万行级别上做同一件事

数据量上来以后,Pandas 的窗口函数写法会吃内存。同一套逻辑放到 SQL 里更稳。

-- 计算单季值与滚动12个月经营现金流 WITH base AS ( SELECT 股票代码, 报表日期, 经营活动产生的现金流量净额 AS ocf, LAG(经营活动产生的现金流量净额, 4) OVER (PARTITION BY 股票代码 ORDER BY 报表日期) AS ocf_yoy FROM cashflow_panel WHERE 报表类型 = '合并报表' ) SELECT 股票代码, 报表日期, ocf - ocf_yoy AS ocf_ttm_增量口径, AVG(ocf) OVER (PARTITION BY 股票代码 ORDER BY 报表日期 ROWS BETWEEN 3 PRECEDING AND CURRENT ROW) AS ocf_ma4 FROM base;

逻辑与参数:LAG(..., 4)取的是去年同期的那一行,因为一年有四个报告期(Q1、半年、Q3、年报),往前推4行正好落在同一个报告期。ROWS BETWEEN 3 PRECEDING AND CURRENT ROW是4个报告期的移动平均,由于每行都是累计值,这个平均值不代表任何会计期间,只能当平滑后的趋势量使用。要真正得到滚动12个月的现金流,正确公式是:最近一期年报累计 + 本期累计 − 去年同期累计,2024-06-30 对应的就是2023年报 + 2024半年 − 2023半年

4. 现金流结构筛选实战:正负号组合、行业基准与滚动窗口

指标算完只是半成品。用现金流做筛选,最有效的不是单个比值的阈值,而是三大活动现金流的符号组合。

4.1 八种现金流符号组合与生命周期含义

经营活动、投资活动、筹资活动各取正负,共八种状态。金融业要单独处理,下面讨论的都是非金融企业。

经营投资筹资典型含义需要警惕的点
++扩张期,主业造血且持续融资投入筹资规模远超经营现金流时靠外部输血
+成熟期,自我造血并偿还债务或分红投资大幅收缩可能是无项目可投
++收缩期,变卖资产还债投资现金流入来自处置长期资产要确认
+++现金充裕但缺乏投向长期停留在此状态说明资本配置效率低
+烧钱扩张,全靠融资最需要预警的组合
++主业失血,靠卖资产和借款维持风险极高
+主业失血同时还债流动性危机前兆
三线净流出一般只出现在季末调头寸或重大并购当期
def sign(x): return np.where(x > 0, 1, np.where(x < 0, -1, 0)) df["cf_pattern"] = (sign(df["经营活动产生的现金流量净额"]).astype(str) + "/" + sign(df["投资活动产生的现金流量净额"]).astype(str) + "/" + sign(df["筹资活动产生的现金流量净额"]).astype(str)) # 筛选连续两期落在烧钱扩张组合的样本 df = df.sort_values(["股票代码", "报表日期"]) df["prev_pattern"] = df.groupby("股票代码")["cf_pattern"].shift(1) risk = df[(df["cf_pattern"] == "-1/-1/1") & (df["prev_pattern"] == "-1/-1/1")]

逻辑说明:sign用嵌套 where 而不是np.sign,是为了把零单独归为0类,避免数值上极接近零的科目被误判为正或负。实战中建议给一个容差,比如绝对值小于营业收入的1%就归零,否则小额尾差会让组合在正负之间反复跳。groupby().shift(1)生成上一期组合,两期联合筛选能过滤掉单期并表造成的偶发形态。

4.2 行业内分位基准:别用全市场均值

前面反复提过,收现比和净现比有强行业属性。全市场均值的做法等于把白酒和建筑装饰平均到一起,两边都不像。正确姿势是先按申万一级行业分组,再按报告期算分位。

df["收现比_行业分位"] = (df.groupby(["行业名称", "报表日期"])["收现比"] .rank(pct=True, na_option="keep")) # 只保留行业样本数足够的分组,样本太少的行业分位没有统计意义 n = df.groupby(["行业名称", "报表日期"])["收现比"].transform("count") df.loc[n < 15, "收现比_行业分位"] = np.nan

参数说明:pct=True得到0到1的分位,na_option="keep"保证原有的缺失值不参与排名也不被填成中间值。n < 15这个阈值不是硬规定,行业分类越细需要的最小样本越大,二级行业建议放到30。2024年6月这一期要注意,部分行业半年报披露延迟,截面当天样本数可能明显低于年报期,脚本里最好加一条日志把每期实际样本数打出来。

4.3 滚动 12 个月与半年报口径处理

单期现金流量表受季节影响极大,尤其是零售、建筑这类Q4集中回款的行业。用半年报数据直接做同比,看不出趋势。稳妥做法是统一换成滚动12个月(TTM)。

df = df.sort_values(["股票代码", "报表日期"]) g = df.groupby("股票代码") df["ocf_ttm"] = np.where( df["报表日期"].dt.month == 12, df["经营活动产生的现金流量净额"], # 年报本身就是12个月 g["经营活动产生的现金流量净额"].shift(4) # 上年年报累计 + df["经营活动产生的现金流量净额"] # 本期累计 - g["经营活动产生的现金流量净额"].shift(2) # 上年同期累计 )

逻辑与参数:三个分支对应不同报告期类型。年报期直接用累计数,不用拼。季报和半年报期用"上年年报 + 本期累计 − 上年同期累计",shift(2)对半年报来说正好是上年半年报,对Q1来说是上年Q1(此时 shift(2) 落在上上年Q3,需要按报告期类型分别取 shift 步数)。实际写的时候别用一刀切的 shift,按报表日期.dt.month分成 3、6、9、12 四套分支,步数分别是 2、2、2、0 相对年报位置计算,写错一步整列数据就废了。

4.4 银行、保险、券商要单独一档

金融业的报表结构和一般工商业完全不同。银行没有"销售商品、提供劳务收到的现金",也没有"购买商品、接受劳务支付的现金",它的经营活动现金流主要受存贷款规模变动影响,一家银行经营现金流为负可能是放贷扩张,属于正常经营,用工商业的组合表去套会得到完全错误的结论。

做法是在面板上加一列is_financial,金融业样本单独建一套指标:用"经营活动现金流净额 / 平均总资产"代替净现比,用"收取利息、手续费及佣金的现金 / 利息收入"代替收现比。分位数基准也按银行、保险、多元金融三个二级组分别算。这一步省不得,否则每次跑全市场筛选,金融板块都会排在头部或尾部,把选股结果整体带偏。

5. 让 1991-2024年6月 的现金流面板可复现:校验、参数与常见误用

数据管道跑通一次不算成功,能重复跑出同一份结果才算。下面三组校验建议直接写进脚本,每次更新数据自动执行。

5.1 三组校验:恒等式、跨表勾稽、时序连续性

第一组是恒等式。现金流量表内部必须满足:现金及现金等价物净增加额 = 经营活动净额 + 投资活动净额 + 筹资活动净额 + 汇率变动影响;期末现金及现金等价物余额 = 期初余额 + 净增加额。这两条是硬约束,不满足的行一定能定位到取数或单位问题。

def check_identity(df, tol=1e-6): left = df["现金及现金等价物净增加额"] right = (df["经营活动产生的现金流量净额"] + df["投资活动产生的现金流量净额"] + df["筹资活动产生的现金流量净额"] + df["汇率变动对现金的影响"].fillna(0)) bad = df.loc[(left - right).abs() > tol * df["总资产"].clip(lower=1)] return bad[["股票代码", "报表日期", "现金及现金等价物净增加额"]] problem = check_identity(df) print(f"恒等式不通过行数: {len(problem)}")

参数说明:tol * 总资产用的是相对容差,因为不同公司金额量级差几个数量级,固定绝对容差对小公司太松、对大公司太紧。汇率变动对现金的影响只有外币业务较多的公司才非空,所以fillna(0)

第二组是跨表勾稽。现金流量表补充资料里披露的"净利润"应当和利润表的净利润一致,允许有少数股东损益调整的差异但不应超过千分之一。第三组是时序连续性,同一股票代码的期末现金余额应当和下一期期初余额对得上,断层往往意味着数据源在中间年份用了不同供应商的表,或者当年有会计差错追溯重述。

df = df.sort_values(["股票代码", "报表日期"]) df["prev_end"] = df.groupby("股票代码")["期末现金及现金等价物余额"].shift(1) gap = df[(df["期初现金及现金等价物余额"] - df["prev_end"]).abs() > df["期末现金及现金等价物余额"].abs() * 0.01 + 1] print(gap[["股票代码", "报表日期"]].head(20))

逻辑说明:加+1是为了让金额接近零的公司不至于因为四舍五入被判为断层。查出来的断层样本别急着手工修,先去核对是不是报告期不连续(比如中间某年停牌未披露),确认是原始缺失后,在面板上打缺口标记,指标计算时跳过,而不是插值填补——插值在现金流序列上会造出不存在的平滑趋势。

5.2 关键参数建议表

下面这些参数是我自己跑这份1991-2024年6月面板时会固定下来的一套,可以直接抄走再按行业微调。

参数建议值理由
现金流指标起算期1998年以后之前披露的是财务状况变动表
收现比分母增值税率13%,农林牧渔用9%现金流含税,口径要匹配
净现比有效样本净利润 > 0 且营业收入 > 0否则比值无经济含义
行业分位最小样本一级行业15,二级行业30样本太少分位不稳定
符号组合容差科目绝对值 / 营业收入 < 1% 归零消除尾差导致的形态抖动
恒等式容差总资产的 1e-6相对容差,量级无关
时序断层阈值期末余额绝对值的1% + 1元兼顾零值公司

表格里最容易被忽略的是符号组合容差。不加这个,很多公司的经营现金流在几百万和-几百万之间来回跳,形态标签一年一变,基于形态的因子完全没有预测力。

5.3 一个具体技巧:用5年累计值代替单年值做筛选

单年净现比噪声大,尤其对项目制、周期类行业。把窗口拉长到5年,用累计经营现金流除以累计净利润,能有效平滑掉单年的大额预收或集中回款。

df = df.sort_values(["股票代码", "报表日期"]) g = df.groupby("股票代码") df["ocf_5y"] = g["经营活动产生的现金流量净额"].transform( lambda s: s.rolling(window=20, min_periods=16).sum()) # 20个报告期≈5年 df["np_5y"] = g["净利润"].transform( lambda s: s.rolling(window=20, min_periods=16).sum()) df["净现比_5y"] = np.where(df["np_5y"] > 0, df["ocf_5y"] / df["np_5y"], np.nan) # 落地筛选:五年累计净现比大于1,且最近一期收现比在行业中位数以上 candidates = df[(df["净现比_5y"] > 1) & (df["收现比_行业分位"] > 0.5) & (df["报表日期"] == "2024-06-30")]

逻辑与参数:窗口取20个报告期对应5年,min_periods=16允许公司中途上市,只要凑够4年数据就参与计算,否则会把2019年以后上市的公司全部剔掉,样本量损失很大。注意这是累计值口径,滚动求和得到的是5年累计经营现金流,和前面 TTM 的算法不是一回事,两列不要混用。用这种方式筛出来的名单,在周期顶部的假繁荣阶段,比单年净现比阈值稳定得多。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询