简介:这份资源是CSMAR中国上市公司财务报表数据库的说明文档,面向会计、金融、经济管理领域的研究者与数据分析人员,用于了解该数据库的结构设计与字段规范。文档以2006年企业会计准则为框架,涵盖财务年报、中报、季报、金融财务及B股财务等数据,并按资产负债表、利润表、现金流量表直接法与间接法重新分类,适用于一般行业及银行、保险、证券等特殊行业。资源包共1个doc文件,大小约856KB,内容包含数据库简介、字段提取方式、报表类型说明及财务附注库的29张表、430个字段等细节,便于读者快速掌握会计期间区分、历史数据兼容及新增科目列示等要点。目前已有211人学习下载,适合需要理解财务报表数据结构、开展实证研究或搭建分析模型的人员参考。
1. 从一份“CSMAR中国上市公司财务报表数据库.doc”说起:它到底是什么,能解决什么问题
如果你手里拿到一份名为“CSMAR中国上市公司财务报表数据库.doc”的文件,第一反应大概率是:这是一份数据字典,还是一份导出的数据表?实际上,它更可能是一份数据库说明文档,里面记录了表结构、字段含义、单位、报告期格式、缺失值约定,以及若干张核心财务报表的字段清单。它解决的不是“数据从哪来”,而是“数据怎么读、怎么对齐、怎么避免把单位搞错”。
这类文档最常见的落地场景是:某高校课题组或某公司研究团队,需要把CSMAR的财务数据导入本地数据库,做实证分析、因子回测或行业对比。适合谁?适合已经拿到数据、但被字段名、合并键、单位、报告期格式卡住的人。如果你正准备从零搭建一个财务数据仓库,这份文档就是你的“黑匣子说明书”——不看它,后面全是玄学。
2. 把.doc读成可查询的数据字典:字段解析与表结构还原
2.1 先别急着写代码,把文档里的表结构抽出来
拿到一份.doc格式的数据库说明,第一步不是打开Python,而是先把它转成可检索的文本。常见做法是:用python-docx读取段落和表格,把每个字段的“字段名、中文含义、类型、单位、备注”抽成结构化数据。很多文档里字段说明是以表格形式存在的,直接复制粘贴会丢格式,所以用脚本抽更稳。
# 用 python-docx 抽取 .doc/.docx 中的表格与段落 # 注意:老式 .doc 需要先另存为 .docx,python-docx 不直接支持 .doc from docx import Document import pandas as pd doc = Document("CSMAR中国上市公司财务报表数据库.docx") rows = [] # 遍历文档中的所有表格,假设字段说明在表格里 for table in doc.tables: for row in table.rows: cells = [c.text.strip() for c in row.cells] # 只保留至少有4列的行,避免把标题行也收进来 if len(cells) >= 4: rows.append(cells[:5]) df = pd.DataFrame(rows, columns=["字段名", "中文含义", "类型", "单位", "备注"]) df = df.drop_duplicates(subset=["字段名"]) df.to_csv("csmar_finance_dict.csv", index=False, encoding="utf-8-sig") print(df.head(10))这段代码的逻辑很直接:把文档里所有表格行拉平,按列切分,去重后落成CSV。参数上唯一需要调的是cells[:5]——如果你的文档表格列数更多,比如还有“是否主键”“取值范围”,就把切片改成cells[:7],同时把columns列表补全。跑完之后,你会得到一份可以grep、可以merge的字段字典,后面写SQL或Pandas时直接查。
注意:老式
.doc文件不能直接被python-docx读取,先用Word或LibreOffice另存为.docx,这一步没有捷径。
2.2 识别三张核心表:资产负债表、利润表、现金流量表
CSMAR的财务报表数据库通常按报表类型分表,常见的是资产负债表、利润表、现金流量表,外加一个公司基本信息表。文档里会给出每张表的字段清单,但不会告诉你“哪几个字段是合并键”。我一般会从文档里先找这几类字段:
| 字段类型 | 常见字段名示例 | 作用 |
|---|---|---|
| 公司标识 | Stkcd、Symbol | 跨表合并的主键 |
| 报告期 | Accper、EndDate | 区分年报、季报 |
| 报表类型 | Typrep | 区分合并报表与母公司报表 |
| 金额字段 | A001000000、B001100000 | 具体科目,单位通常是元 |
| 币种 | Currency | 多数为CNY,少数为其他 |
文档里如果写了“A001000000 代表货币资金”,你就得把这个字段名和中文含义对应起来。很多新手直接拿字段名当列名用,结果写SQL时完全不知道A001000000是什么,这就是没读文档的代价。
2.3 用Pandas把字段字典和实际数据对齐
拿到字段字典后,下一步是把它和你实际下载的数据文件对齐。常见做法是:读入一个样本CSV,检查列名是否和字典里的“字段名”一致,不一致的做映射。
import pandas as pd # 读入字段字典和实际数据样本 dict_df = pd.read_csv("csmar_finance_dict.csv") sample_df = pd.read_csv("sample_balance_sheet.csv", nrows=100) # 找出实际数据里有、但字典里没有的列 actual_cols = set(sample_df.columns) dict_cols = set(dict_df["字段名"].dropna()) missing_in_dict = actual_cols - dict_cols print("数据里有但字典没记录的列:", missing_in_dict) # 找出字典里有、但实际数据没出现的列(可能是该表不包含) missing_in_data = dict_cols - actual_cols print("字典有但数据没出现的列数量:", len(missing_in_data))这段代码的价值在于:它能帮你快速发现“文档和实际数据不一致”的情况。比如文档里写的是Accper,实际数据里可能是AccPer,大小写差异会导致合并失败。参数上,nrows=100只是抽样,确认无误后可以去掉。如果发现大量字段对不上,优先怀疑是报表类型或报告期筛选错了,而不是数据本身有问题。
3. 从文档到本地数据库:建表、导入与报告期对齐
3.1 建表时把单位统一成“元”,别在分析阶段再换算
文档里通常会注明“金额单位:元”,但有些导出文件可能是“万元”或“千元”。我一般会在建表阶段就统一成元,避免后面做比率分析时反复乘除。建表SQL可以这样写:
-- 以资产负债表为例,建一张本地表 CREATE TABLE balance_sheet ( stkcd VARCHAR(10) NOT NULL, -- 公司代码 accper DATE NOT NULL, -- 报告期 typrep VARCHAR(2), -- 报表类型:A=合并,B=母公司 currency VARCHAR(10) DEFAULT 'CNY', a001000000 DECIMAL(20,2), -- 货币资金,单位:元 a002000000 DECIMAL(20,2), -- 应收账款 a003000000 DECIMAL(20,2), -- 存货 PRIMARY KEY (stkcd, accper, typrep) );主键设计是关键:stkcd + accper + typrep能唯一确定一行。如果你只按stkcd + accper建主键,合并报表和母公司报表会冲突。参数上,DECIMAL(20,2)足够容纳大多数上市公司的金额,但如果你要存银行或保险公司的数据,建议改成DECIMAL(24,2)。typrep字段一定要保留,否则后面做合并报表分析时还得回去翻原始文件。
3.2 用Python批量导入CSV,处理编码和日期格式
CSMAR导出的CSV常见编码是gbk或utf-8-sig,日期格式可能是YYYY-MM-DD,也可能是YYYYMMDD。导入时如果不处理,后面按报告期筛选就会翻车。
import pandas as pd from sqlalchemy import create_engine engine = create_engine("mysql+pymysql://user:pass@localhost:3306/finance_db") # 读入时指定编码,避免中文列名乱码 df = pd.read_csv("balance_sheet_2023.csv", encoding="gbk") # 统一报告期格式 df["accper"] = pd.to_datetime(df["accper"].astype(str), format="%Y%m%d", errors="coerce") # 金额字段转数值,去掉千分位逗号 for col in ["a001000000", "a002000000", "a003000000"]: df[col] = pd.to_numeric(df[col].astype(str).str.replace(",", ""), errors="coerce") # 写入数据库,按主键冲突则替换 df.to_sql("balance_sheet", engine, if_exists="append", index=False, chunksize=5000)逻辑说明:pd.to_datetime里的format="%Y%m%d"要按实际格式改,如果数据是2023-12-31,就改成format="%Y-%m-%d"。errors="coerce"会把无法解析的日期变成NaT,方便后面排查。chunksize=5000是为了避免一次性写入太大导致内存溢出。如果主键冲突报错,说明数据里有重复行,需要先按主键去重再写入。
3.3 报告期对齐:年报、季报别混在一起算
文档里会说明Accper字段的含义,但不会告诉你“季报的累计值和单季值怎么区分”。常见坑是:把一季报的营业收入直接和年报比,结果同比增速完全不对。我一般会在ETL阶段加一个report_type字段,标记“Q1、Q2、Q3、Annual”,并且只在对齐后再做计算。
# 根据报告期月份标记报表类型 df["report_type"] = df["accper"].dt.month.map({ 3: "Q1", 6: "Q2", 9: "Q3", 12: "Annual" }) # 只保留年报做年度分析 annual_df = df[df["report_type"] == "Annual"].copy() # 如果要算单季值,用累计值做差分 df = df.sort_values(["stkcd", "accper"]) df["single_quarter_revenue"] = df.groupby("stkcd")["revenue"].diff()这段代码的关键是groupby("stkcd").diff()——它把同一公司的累计值转成单季值。参数上,sort_values必须先按公司和报告期排序,否则差分结果会错位。如果你不做这一步,直接拿累计值做环比,出来的数字连自己都不敢信。
4. 避坑与排查:字段、单位、报告期最容易翻车的5个地方
4.1 现象:合并报表和母公司报表混在一起,资产总额翻倍
原因:Typrep字段没做过滤,直接把两种报表类型都导入了同一张表。解决:在ETL阶段加WHERE typrep = 'A',或者在Pandas里df = df[df["typrep"] == "A"]。如果你确实需要两种报表,就建两张表,别混在一起。
4.2 现象:金额字段出现负数,但文档里写的是“正数表示借方”
原因:CSMAR的某些科目用正负号区分借贷方向,文档里会说明,但新手容易忽略。解决:先读文档里的“备注”列,确认符号含义。如果确实需要统一方向,在ETL阶段对特定科目做abs()或乘-1,但一定要在代码里写注释,否则半年后自己都忘了为什么。
4.3 现象:报告期是“2023-12-31”,但导入数据库后变成“2024-01-01”
原因:时区或日期解析格式不对。解决:用pd.to_datetime时显式指定format,不要依赖自动推断。如果数据里只有日期没有时间,导入MySQL时用DATE类型,不要用DATETIME,避免时区偏移。
4.4 现象:字段名大小写不一致,合并时对不上
原因:文档里写Accper,实际数据里是AccPer,或者反过来。解决:在读取CSV后统一转成小写或大写,再做映射。我一般会在ETL脚本开头加一行df.columns = [c.lower() for c in df.columns],然后字典里的字段名也统一小写。
4.5 现象:缺失值被当成0,算出来的比率完全失真
原因:CSMAR里缺失值可能是空字符串、NULL、-9999或0,文档里会说明。解决:读文档里的“缺失值约定”,在Pandas里用na_values参数指定。比如pd.read_csv(..., na_values=["", "NULL", "-9999"]),然后再做填充或剔除。千万别直接fillna(0),否则资产负债率可能算出负数。
5. 进阶用法:用字段字典自动生成建表语句与校验规则
5.1 从字段字典生成DDL,减少手写错误
当你把文档里的字段字典抽成CSV后,可以进一步用它生成建表语句。这样每次新增一张表,不用手动敲字段名和类型。
import pandas as pd dict_df = pd.read_csv("csmar_finance_dict.csv") # 只保留需要的字段,并做类型映射 type_map = {"数值": "DECIMAL(20,2)", "文本": "VARCHAR(50)", "日期": "DATE"} ddl_lines = ["CREATE TABLE auto_generated (", " id INT AUTO_INCREMENT PRIMARY KEY,"] for _, row in dict_df.iterrows(): field = row["字段名"] col_type = type_map.get(row["类型"], "VARCHAR(100)") comment = row["中文含义"] ddl_lines.append(f" {field} {col_type} COMMENT '{comment}',") ddl_lines.append(" UNIQUE KEY uk_stkcd_accper (stkcd, accper)") ddl_lines.append(");") print("\n".join(ddl_lines))这段代码的价值在于:字段字典一变,DDL跟着变,不用手动同步。参数上,type_map要根据文档里的实际类型描述调整,比如“字符”对应VARCHAR,“金额”对应DECIMAL。UNIQUE KEY那行需要你根据实际主键改,别直接抄。
5.2 用校验规则做数据质量检查
建完表之后,我一般会跑几条校验SQL,确认数据没有明显异常。比如:
-- 检查同一公司同一报告期是否有重复行 SELECT stkcd, accper, COUNT(*) AS cnt FROM balance_sheet GROUP BY stkcd, accper, typrep HAVING cnt > 1; -- 检查资产总计是否等于负债加所有者权益(允许一定误差) SELECT stkcd, accper, a001000000, a002000000 FROM balance_sheet WHERE ABS(a001000000 - (a002000000 + a003000000)) > 1;第一条查重复,第二条查勾稽关系。参数上,ABS(...) > 1里的1是容忍度,因为四舍五入可能差几分钱。如果差得离谱,说明字段映射错了,回去翻文档。
5.3 一个我踩过的坑:别把文档里的“示例值”当成真实数据
有一次我在文档里看到某个字段的示例值是1000000,就以为所有公司的货币资金都在百万级别,结果建表时把类型设成DECIMAL(10,2),导入时直接溢出。后来才发现,文档里的示例只是随便写的,真实数据里有的公司货币资金是几十亿。所以,建表类型一定要按业务上限来设,别按文档示例来。我现在的习惯是:金额字段一律DECIMAL(20,2),文本字段一律VARCHAR(100)起步,不够再改。
希望帮到你。
本文还有配套的精品资源,点击获取