简介:这份资源是CSMAR中国上市公司财务报表数据库的说明文档,面向会计、金融、经济管理领域的研究者、高校师生及数据分析从业者,用于了解该数据库的结构设计与字段规范,辅助开展上市公司财务数据的实证研究与建模分析。包内仅含1个doc文件,大小约856KB,以文字与表格形式系统梳理数据库的报表分类、字段提取方式及数据范围,便于快速查阅与引用。文档详细介绍了以2006年企业会计准则为框架的报表体系,涵盖资产负债表、利润表、现金流量表直接法与间接法文件,并说明如何用「会计期间」区分年、中、季报及年初年末数据,同时讲解新旧准则下股东权益、非流动资产等科目的对应处理,以及交易性金融资产、商誉等新增科目的列示方式。此外还涉及财务附注数据库的29张表、430个字段及应收款项账龄明细等内容。目前已有211人学习,适合需要理解CSMAR数据结构、规范财务指标口径的研究人员参考使用。
1. 从一份“能改的 doc”说起:CSMAR 财务库到底装了什么
如果你正在做上市公司财务方向的实证研究,大概率绕不开一个名字——CSMAR。但很多人第一次拿到手的,不是数据库客户端,而是一份CSMAR中国上市公司财务报表数据库.doc。别小看这份文档,它其实是整套数据库的“说明书+字段字典+结构设计稿”,把财务年报、中报、季报、金融财务、B股财务五大库的字段逻辑、报表分类、会计期间规则全写进去了。它解决的核心问题是:让你在正式跑回归之前,先搞清楚每个字段从哪来、怎么对齐、跨年怎么比。适合谁?适合正在做面板数据清洗、财务指标构建、审计意见文本分析的研究生和量化从业者。我见过太多人跳过这份文档直接开跑,结果在“股东权益合计”这种科目上翻车——2007 年前后口径完全不同,不读文档根本发现不了。
2. 会计期间与报表类型:两个字段决定你能不能对齐面板
2.1 会计期间不是日期,是“报表身份”
很多人把“会计期间”当成普通日期字段,直接pd.to_datetime一转就完事。这是第一个大坑。在这套库里,会计期间承担的是“报表身份标识”的功能,它同时区分三件事:年/中/季报、年初/年末、以及是否属于同一财政年度。
文档里写得很清楚:2007-12-31表示 2007 年年报,2007-03-31表示 2007 年第一季季报,而2007-01-01表示的是 2007 年年初数据——注意,年初数据不是上年年末,它是资产负债表期初列报值。如果你把年初和年末混在一起做时间序列,整个面板的滞后项全错。
常见做法是先把会计期间拆成三个辅助列:
import pandas as pd # 假设原始表 df 中有 '会计期间' 和 '报表类型' 两列 df['会计期间'] = pd.to_datetime(df['会计期间']) # 1. 报表频率:根据月日判断 def freq_flag(dt): md = (dt.month, dt.day) if md == (12, 31): return '年报' elif md == (6, 30): return '中报' elif md in [(3, 31), (9, 30)]: return '季报' elif md == (1, 1): return '年初' else: return '其他' df['报表频率'] = df['会计期间'].apply(freq_flag) # 2. 财政年度:直接取年份 df['财政年度'] = df['会计期间'].dt.year # 3. 时点标记:年初 or 年末 df['时点'] = df['会计期间'].apply( lambda x: '年初' if (x.month, x.day) == (1, 1) else '期末' )逻辑说明:freq_flag把 12-31、6-30、3-31/9-30、1-1 分别映射到年报、中报、季报、年初。参数上唯一需要留意的是,部分早期数据可能存在非标准日期(比如 2001 年之前的季报披露不规范),跑完value_counts()后如果出现“其他”,要单独拉出来核对,别直接 drop。
2.2 报表类型 A/B:合并与母公司的分叉口
报表类型只有两个值:A=合并报表,B=母公司报表。看起来简单,但选错类型是高频翻车点。合并报表包含子公司,母公司报表只反映母公司自身。做偿债能力分析通常用合并报表,做母公司现金流归集可能用 B。文档里所有示例默认给的是 A,但你的研究设计如果涉及“母公司对子公司的资金占用”,就必须切到 B。
我一般会在数据加载阶段就按报表类型拆成两张宽表,避免后续 merge 时把 A 和 B 混在一起:
# 按报表类型拆分 df_a = df[df['报表类型'] == 'A'].copy() df_b = df[df['报表类型'] == 'B'].copy() # 检查每个证券代码在两种类型下的记录数 check = df.groupby(['证券代码', '报表类型']).size().unstack(fill_value=0) print(check.describe())如果发现某些证券代码只有 A 没有 B,或者 B 的记录数远少于 A,这是正常的——早期很多公司不披露母公司报表。但如果你做的是配对样本研究,就要在样本筛选阶段把不配对的公司剔除,而不是在回归阶段才发现样本量对不上。
3. 科目口径变迁:2007 年前后怎么接、怎么断
3.1 “股东权益合计”的口径断裂
这是文档里花最大篇幅讲的一件事,也是实际清洗中最容易出错的地方。2007 年新准则之前,“股东权益合计”只包含归属于母公司所有者的权益,少数股东权益是单独列示的,不属于“股东权益合计”。2007 年之后,新准则把少数股东权益并入了“股东权益合计”,同时新增了“归属于母公司所有者权益合计”这个字段。
这意味着什么?如果你直接拿 2005 年和 2008 年的“股东权益合计”做纵向对比,2005 年的值偏小,2008 年的值偏大,差异不是公司经营变化,而是口径变了。
处理方式有两种。第一种是统一到旧口径:2007 年之后的数据,用“归属于母公司所有者权益合计”替代“股东权益合计”。第二种是统一到新口径:2007 年之前的数据,把“股东权益合计”加上“少数股东权益”。我一般用第一种,因为“归属于母公司”这个口径在后续的 ROE 计算中更常用。
# 统一股东权益口径(统一到归属于母公司口径) df['股东权益_统一'] = df.apply( lambda row: row['归属于母公司所有者权益合计'] if pd.notna(row.get('归属于母公司所有者权益合计')) else row['股东权益合计'], axis=1 ) # 标记口径来源,方便回溯 df['权益口径'] = df['归属于母公司所有者权益合计'].apply( lambda x: '新准则' if pd.notna(x) else '旧准则' )参数说明:归属于母公司所有者权益合计这个字段在 2007 年之前的记录中为空,所以pd.notna判断是安全的。但要注意,部分 2007 年之后的记录也可能因为披露缺失而为空,这时候 fallback 到“股东权益合计”会引入口径不一致,建议对这部分记录打标后单独检查。
3.2 取消科目的保留与映射
文档提到,“短期投资”等旧准则科目在新准则中取消了,但数据库采取了保留策略。这对做长面板的人是好事——你不用去别的地方找 2006 年之前的短期投资数据。但问题是,新准则下的“交易性金融资产”和旧准则的“短期投资”并不是一一对应关系。
常见做法是建一张科目映射表,把旧科目映射到新科目体系下最接近的类别,同时在论文里说明映射规则。比如:
| 旧准则科目 | 新准则对应科目 | 映射说明 |
|---|---|---|
| 短期投资 | 交易性金融资产 | 仅当持有目的为交易性时 |
| 长期投资合计 | 非流动资产合计 | 文档明确归入非流动资产 |
| 固定资产合计 | 固定资产净额 | 口径基本一致 |
| 无形资产合计 | 无形资产净额 | 需扣除累计摊销差异 |
这张表不是让你直接替换字段,而是让你在构建跨年指标时知道哪些科目可以合并、哪些必须分开。我见过有人把“长期投资合计”和“持有至到期投资”加在一起算,结果 2007 年前后重复计算,指标直接爆炸。
4. 财务指标与附注库:208 个指标怎么选、29 张表怎么连
4.1 同一指标多种算法:A 版和 B 版的区别
财务指标分析库里,同一个指标名可能对应多个算法。文档以“应收账款周转率”和“存货周转率”为例:A 版用期末余额做分母,B 版用平均占用额做分母。这不是可有可无的选项,而是直接影响你结论的变量定义。
# 应收账款周转率 A 版:营业收入 / 应收账款期末余额 df['应收账款周转率_A'] = df['营业收入'] / df['应收账款净额'] # 应收账款周转率 B 版:营业收入 / 应收账款平均占用额 df['应收账款平均占用额'] = (df['应收账款净额'] + df['应收账款净额'].shift(1)) / 2 df['应收账款周转率_B'] = df['营业收入'] / df['应收账款平均占用额']逻辑说明:B 版需要按证券代码分组后做 shift,否则会把不同公司的数据混在一起。正确写法是先sort_values(['证券代码', '会计期间']),再groupby('证券代码')['应收账款净额'].shift(1)。参数上,如果某公司中间有年度缺失,shift 会跨年错位,建议在 shift 之前先补齐年度索引或标记缺失。
选 A 还是 B?如果样本期内公司规模波动大,用 B 更稳健;如果只是做截面分析,A 更直接。关键是全文统一,别在描述性统计用 A、回归用 B。
4.2 附注库的 29 张表:常规与特殊分开处理
财务附注库有 29 张表、430 个字段,文档特别强调“常规性和特殊性数据分开”。比如应收款项表里,常规明细按账龄划分,特殊明细单独成文件记录特殊计提。这个设计的好处是:你做建模分析时,直接用常规表批量提取;特殊表只在做案例研究或异常检测时翻出来看。
我一般会先把 29 张表的表名和主键列出来,建一张元数据表:
# 伪代码:遍历附注库所有表,记录表名、行数、主键候选 import sqlite3 conn = sqlite3.connect('csmar_notes.db') tables = pd.read_sql( "SELECT name FROM sqlite_master WHERE type='table'", conn ) meta = [] for t in tables['name']: cnt = pd.read_sql(f"SELECT COUNT(*) as c FROM {t}", conn)['c'][0] cols = pd.read_sql(f"PRAGMA table_info({t})", conn)['name'].tolist() meta.append({'表名': t, '行数': cnt, '字段数': len(cols), '字段列表': cols}) meta_df = pd.DataFrame(meta) print(meta_df.sort_values('行数', ascending=False).head(10))这样你能快速知道哪张表记录最多(文档提到主营业务收入有 84 万条)、哪张表字段最宽,从而决定哪些表进主分析、哪些表做补充验证。
5. 避坑与排查:那些文档没写但一定会遇到的问题
5.1 现象:合并报表和母公司报表的证券代码相同,merge 后行数翻倍
原因:直接按证券代码 merge 两张表,没有加报表类型约束。解决:所有 merge 操作必须带上on=['证券代码', '会计期间', '报表类型'],或者提前把 A/B 拆成独立数据集。
5.2 现象:2007 年之前的“交易性金融资产”全为空
原因:该科目是新准则新增,旧准则下不存在。解决:不要用fillna(0)直接填零,零和缺失在财务上含义完全不同。正确做法是保留 NaN,在构建指标时用条件判断处理,比如“交易性金融资产占比”只在 2007 年之后计算。
5.3 现象:审计意见库中同一公司同一年有两条记录
原因:境内审计机构和境外审计机构分别记录,审计费用也分境内和境外。解决:如果研究的是审计意见类型,去重时按证券代码+审计日期保留一条;如果研究的是审计费用,要把境内和境外费用加总,注意货币单位可能不同。
5.4 现象:分析师预测库中预测终止日相同但分析师不同,直接平均后偏差大
原因:不同分析师的预测精度和覆盖范围差异很大,简单平均会引入噪声。解决:常见做法是按研究机构分组取中位数,或者只保留预测发布日在报告公布日之前一定窗口内的记录。文档里提供了实际指标文件,可以用来计算预测误差,反过来筛选分析师样本。
5.5 现象:银行财务库和一般行业财务库的资产负债表科目名不同,合并后大量缺失
原因:银行报表结构特殊,科目设置与一般行业不一致。解决:不要试图把银行样本和一般行业样本混在一张宽表里跑回归。要么分开建模,要么只保留两套体系共有的科目(如总资产、净利润),并在论文中说明样本划分依据。
6. 从 doc 到可复现流程:我习惯先跑一遍字段审计
拿到任何一份 CSMAR 的 doc 或数据导出文件,我第一件事不是跑回归,而是做字段审计。具体做法是:把 doc 里列出的字段清单和实际数据文件的列名做一次 diff,找出三类问题——doc 有但数据没有的、数据有但 doc 没提的、名称不一致的。
# 字段审计:doc 字段清单 vs 实际数据列名 doc_fields = [ '证券代码', '会计期间', '报表类型', '货币资金', '流动资产合计', '固定资产净额', '无形资产净额', '资产总计' ] # 这里只列了资产负债表的一部分,实际使用时从 doc 全文提取 actual_cols = df.columns.tolist() missing_in_data = set(doc_fields) - set(actual_cols) missing_in_doc = set(actual_cols) - set(doc_fields) print('doc 有但数据没有:', missing_in_data) print('数据有但 doc 没提:', missing_in_doc)这一步能帮你提前发现版本差异。比如某些年份的导出文件可能把“固定资产净额”写成“固定资产净值”,如果不做审计,跑到后面 merge 时才发现对不上,返工成本很高。
另一个习惯是:对每个关键字段跑一遍describe()和缺失率统计,尤其是跨年字段。2007 年前后的缺失模式往往能告诉你口径断裂发生在哪一年、哪些科目受影响最大。
# 关键字段缺失率按年统计 key_fields = ['股东权益合计', '归属于母公司所有者权益合计', '少数股东权益'] missing_by_year = df.groupby('财政年度')[key_fields].apply( lambda g: g.isna().mean() ) print(missing_by_year.round(3))如果“归属于母公司所有者权益合计”在 2006 年缺失率 100%、2007 年骤降到 5% 以下,那就确认了口径切换点。这时候再回头调整你的样本期间或变量定义,比跑完回归再补救要省事得多。
从那以后,我每次拿到新的 CSMAR 数据导出,都强制先走一遍字段审计和缺失率按年扫描,再动任何模型。希望帮到你。
本文还有配套的精品资源,点击获取