简介专精特新‘小巨人’企业财务数据资源包覆盖2013至2024年整合第1至第5批国家级专精特新‘小巨人’企业的财务与名单信息面向经济学研究者、政策制定者、投资分析人员及企业战略规划者可用于分析中小企业成长路径、行业分布特点、政策扶持效果与区域创新格局。资源包共10个文件主要包含Stata可直接读取的dta数据文件、Excel格式的xlsx财务数据表、数据来源与处理说明html、使用说明txt以及6个PDF格式的分批次企业名单文件整体体积36.12MB兼顾统计分析、表格浏览与资料查证需求。目前已有55人学习下载。借助这份数据读者可一站式获取企业营收、利润、资产负债等财务指标及批次、地区、行业等关键属性免去多处收集、手工清洗的繁琐既可用于学术计量分析与政策评估也可为投资筛选、企业对标提供参考是观察中国经济微观层面、尤其是专精特新中小企业发展的重要资料。1. 这份 zip 不是开箱即用的数据先看货再动手做企业授信尽调或产业研报的人看到“专精特新小巨人-财务数据第1-5批2013-2024年.zip”这种文件名第一反应基本是终于不用自己去翻五年名单了。但真正解压之后才会发现压缩包标题里的“第1-5批”和“2013-2024年”只是最乐观的描述——它并不承诺每个批次都覆盖每一年也不保证五批表的字段长一个样。这篇笔记就把这类小巨人财务数据包从头到尾拆一遍先做文件地图再统一字段和主键然后算财务指标、做候选池筛选最后把最容易让分析翻车的几个坑列清楚。适合把这包数据当“候选池”而不是“结果集”的人尤其是做企业融资、产业跟踪和基础风控建模的从业者。2. 拆包与目录结构先看清批次构成再谈清洗2.1 用 infolist 列出压缩包内容先做文件地图拿到这种压缩包我的习惯是暂缓一切读取动作先做一次“只看不动”的文件清单检查。原因很简单第1批和第5批之间隔了好几年中间换过文件名模板、补过数据压缩包里的实际文件数很可能和标题暗示的不一致。直接 pd.read_csv 去猜文件路径是最容易踩空的姿势。想把这个包变成能持续使用的数据资产第一优先是把 zip 的目录完整读出来这步比任何清洗代码都重要。import zipfile from pathlib import Path zip_path Path(专精特新小巨人-财务数据第1-5批2013-2024年.zip) with zipfile.ZipFile(zip_path, r) as zf: for info in zf.infolist(): print(info.filename) print(f 原始大小: {info.file_size} 压缩后: {info.compress_size})这段代码输出的信息足够判断三件事文件是按批次拆分还是按年份拆分有没有“补录”“更新”字样的文件以及单个 CSV 是否大到异常。file_size是未解压的原始体积compress_size是压缩包内占用体积。文本型 CSV 重复内容多压缩率一般很高如果一个“基本信息表”解压出来有几十 MB那里面很可能不只是财务字段而是把明细账也塞了进来后面解析时要按大文件单独处理不能和普通表格走同一套逻辑。还有一类容易被忽略的细节zip 里的文件名用什么编码写入。打包工具在不同操作系统里生成的文件头编码不一致Python 的 zipfile 默认按 cp437 解码中文文件名经常变成乱码。虽然不影响文件内容读取却会让文件地图完全不可读。我一般会在列目录时挂一个名字修正函数先把乱码字符还原成原始字节再用 gbk 重新解释。def fix_zip_name(raw: str) - str: try: return raw.encode(cp437).decode(gbk) except (UnicodeEncodeError, UnicodeDecodeError): return raw这段函数简短作用却很关键encode(cp437)是把当前显示出的乱码文字还原成原始字节decode(gbk)再按中文系统常见的编码去解读文件名就能恢复成可读文本。异常分支的作用是兜底遇到本来就是英文或数字的纯 ASCII 文件名decode 会报错这时直接用原始名字。这一步处理完之后再去匹配公司名单、做后续字段合并才不会被编码不一致搞出两套表。2.2 批次与年份口径先验证是不是平衡面板再合并数据文件清单确认之后下一个问题更关键这些财务年份到底是不是连续覆盖的。所谓平衡面板是指每个企业从 2013 到 2024 每年都有记录。但实际拿到的数据大多是滚动子集第1批企业可能从 2013 或更早开始有财务数据第4批、第5批企业很多是近年才达到相关资质门槛它们没有早期年份的财务记录是正常的。我会在合并之前先对每一个批次单独做一次“年份取值检查”确认该批数据能覆盖哪些年度。import pandas as pd df pd.read_csv( 第1批_财务数据2013-2024.csv, encodingutf-8, dtype{统计年份: str}, ) year_counts df[统计年份].value_counts().sort_index() print(year_counts)把“统计年份”读成字符串而不是数字是刻意为之。CSV 里像 2013 这样的年份有时候会被 Excel 或某些导出工具写成日期格式进入 pandas 后变成“2013-01-01”后续做年份排序和匹配都会出问题。先用字符串读入再在明确需要做年份运算时统一转 int是最稳妥的流程。执行这段代码之后我会看两个指标第一个是最小年份和最大年份第二个是每年企业计数是否出现断裂。如果输出为“2013: 120, 2014: 131, 2015: 0, 2016: 118”说明该批次缺失 2015 年数据后面做同比计算时要特别留意如果本身就是截面数据可能只有文件标题上的一个年度那就更不能用它去做逐年趋势分析。2.3 跨批次同名企业先做名称归一再去判断重复五批名单里同一家企业出现在两个批次里是完全正常的很多企业是第一批认定之后又出现在后面批次的补充名单里。但在数据处理上这属于“重复样本超集”如果直接把五份表 concat 到一起等于把某家企业的财务记录复制多份后面所有汇总统计都会失真。处理重复的第一步不是去重而是先做企业名称归一化。常见做法是把全角括号统一成半角去掉名称首尾空格再把“有限责任公司”这类后缀从比较逻辑里暂时剔除因为不同批次的表可能一个写“某某科技北京有限公司”另一个写“某某科技(北京)股份有限公司”。def normalize_company_name(name: str) - str: if not isinstance(name, str): return name name.strip() name name.replace(, ().replace(, )) name name.replace(股份有限公司, ) name name.replace(有限责任公司, ) return name df[企业标识] df[公司名称].map(normalize_company_name) print(df.groupby(企业标识)[批次].nunique().value_counts())replace(股份有限公司, )这一步会损失精度所以只能在“识别同一家企业”时用不能用它做最终展示名。groupby之后看“批次”字段的非重复计数能直观看到一家企业出现在几个批次里。计数等于 1 的是本批独有大于 1 的是跨批次企业。真正的去重规则要在数据合并阶段再定我常用的方案是保留批次编号最大的那条记录也就是最新一批的财务明细同时把历次出现的批次号合并成一个列表字段方便追溯。3. 字段统一与合并把五批表合成一张可用宽表3.1 表头别名太多用映射字典统一字段名五批文件来自不同时期表头字段名经常对不齐。同一个含义这一批叫“营业收入”下一批叫“营业总收入”再下一批可能叫“营收”。直接 concat 的后果是同一列被拆成三列其中两列全是空值等到做指标计算时数据量凭空少了一大块。我的做法是先写一张字段映射表把每个标准字段名对应的别名列出来再用一个统一函数处理所有读取进来的 DataFrame。field_map { 企业名称: [企业名称, 公司全称, 公司名称], 统计年度: [统计年度, 年度, 会计年度, 年份], 营业收入: [营业收入, 营业总收入, 营收], 净利润: [净利润, 归母净利润, 净利], 总资产: [资产总计, 总资产, 期末资产], 总负债: [负债合计, 总负债, 期末负债], } def normalize_columns(df: pd.DataFrame) - pd.DataFrame: col_mapping {} for std_name, alias_list in field_map.items(): for col in df.columns: if col in alias_list: col_mapping[col] std_name return df.rename(columnscol_mapping)这段逻辑里有两个关键细节。第一个是映射方向判断时用“数据里的列名是否出现在别名列表”而不是反向判断因为一个别名可能出现在多个标准字段下反向判断容易冲突。第二个是返回值我只做 rename 不裁剪列这样万一某批文件里藏着没见过的自定义字段还能保留下来供人工确认。如果确认这个字段没用再在后续处理里显式删除别在统一阶段悄悄丢掉信息。3.2 设计稳定主键企业标识加统计年度字段统一之后下一步是构造主键。主键的作用是给每一行一个唯一身份之后检查重复、匹配外部数据、做透视表都依赖它。对这个数据集最合理的主键是企业标识加统计年度因为财务数据表里一个企业一年应该只有一条有效记录。df_all[主键] df_all[企业标识] _ df_all[统计年度].astype(str) mask_dup df_all.duplicated(subset[主键], keepFalse) print(df_all.loc[mask_dup].sort_values(主键).head(10))duplicated(keepFalse)会把所有重复行都标出来而不只是保留第一条。这样能直接看到完整冲突记录而不是被默认行为悄悄吃掉一半。如果发现主键重复我先不急着删除而是去看重复的两行到底来自哪个批次。通常有两种情况一种是同一批次里自身就重复那多半是原始表有错需要人工确认后删除另一种是不同批次都收录了同一年数据这时按前面说的规则保留最新批次。3.3 用年度跨度判断长面板可用性主键确认干净后可以进一步统计每家企业的年份跨度和记录年数。这个指标决定你能做什么层级的分析。如果一批企业普遍只有一两年财务记录就别想做严格的面板回归如果记录有七八年甚至更长才有资格进入趋势类和增长类分析。span df_all.groupby(企业标识).agg( 首年(统计年度, min), 末年(统计年度, max), 年数(统计年度, nunique), ) print(span.describe())nunique统计的是不同年份数量比count更严谨。count会把同一年的重复行也算进去而nunique能准确告诉你覆盖了几年。首年和末年的差值能看出时间跨度年数能看出记录完整度。一家企业首年 2013、末年 2024、年数只有 5说明中间缺了好几年这种样本在做时间序列时要格外小心最好单独标记成“间隔面板”。看到描述性统计之后我会先在 data 里加一个“记录年数”字段后面做企业筛选时直接用避免反复 groupby。这也是长期维护数据集的好习惯把高频使用的统计结果落成字段而不是每次现算。3.4 长表和宽表怎么选先算指标再决定是否透视很多同事习惯一开始就把数据转成宽表即每家企业一行、每个年份一列觉得这样看起来整齐。但对这种跨十二年且年份不齐的数据过早转宽表会制造大量空值一张几万行的长表透视后可能变成几百万个空单元格。我的建议是在指标计算阶段保持长表需要做矩阵式对比时再按需透视。wide df_all.pivot_table( index企业标识, columns统计年度, values营业收入, aggfuncsum, ) missing_ratio wide.isna().mean(axis1) print(missing_ratio.describe())这段代码的作用不是做最终展示而是快速估计“每家企业在年份维度上的缺失比例”。isna().mean(axis1)对每一行求缺失率然后看整体分布。如果缺失率中位数超过 0.5说明大多数企业只有一半甚至更少的年份有数据这时候趋势分析要降级为“局部年份分析”或者干脆放弃连续增长类指标只做截面财务质量评估。4. 财务指标计算与候选企业筛选拉通数据后先组指标4.1 计算同比和资产负债率先按企业分组再做行内计算长表整理好后就可以开始计算财务指标。最容易翻车的写法是不排序直接算或者把不同企业混在同一组里做 diff。正确的逻辑是先按“企业标识”分组再在组内按“统计年度”排序最后才做环比或同比计算。df_all df_all.sort_values([企业标识, 统计年度]).copy() df_all[营收同比] df_all.groupby(企业标识)[营业收入].pct_change() df_all[资产负债率] df_all[总负债] / df_all[总资产]pct_change()在 groupby 之后默认就是与上一行比加上前面已经按年份排序得到的就是严格意义上的同比增速。第一年因为没有上一期数据计算结果会是 NaN这个不用删后续分析时按需填充。资产负债率是当期总负债除以总资产出现大于 1 的值要警惕不代表数据错可能确实是负资产企业但这类样本需要单独标记。在算这类指标时我会先看两个分布营收同比的 NaN 比例、资产负债率的极值情况。如果 NaN 比例超过一半说明绝大多数企业缺失首年数据后面做“连续增长”筛选时门槛必须放宽否则候选池会缩水到没法用。4.2 三年财务记录作为可分析底线财务分析里有个不成文的底线至少要有三年记录才谈得上“趋势”。两年数据虽然能算出一个同比增速但单点增速受基数影响太大做不了稳健判断。对这批数据我会用“记录年数 3”做第一道筛选。df_all[记录年数] df_all.groupby(企业标识)[统计年度].transform(nunique) df_pool df_all[df_all[记录年数] 3].copy() print(df_pool[企业标识].nunique())用transform而不是agg是为了把统计结果保持在同一张表的行粒度后面筛选条件直接写在原表上不用再合并一次。nunique前面已经解释过能避免同一年重复记录带来的虚高。做完这一步候选池就从“所有出现在名单里的企业”缩到了“财务记录足够支撑分析的企业”。4.3 从“增长但亏损”的筛选逻辑到分层处理做完基础门槛之后就开始碰业务逻辑了。常见的初筛条件是最近一年净利润为正、资产负债率不过高、最近一年营收同比正增长。这三个条件对应的其实是三组不同风险维度盈利质量、偿债压力、成长动能。latest df_pool.sort_values(统计年度).groupby(企业标识).tail(1) candidate latest[ (latest[净利润] 0) (latest[资产负债率] 0.7) (latest[营收同比].fillna(0) 0) ] print(candidate[企业标识].nunique())tail(1)在 groupby 后取的是每组最后一行配合前面按年份排序拿到的就是“最近一年”的数据。三个过滤条件都是比较保守的阈值净利润大于 0 排除亏损企业资产负债率小于 0.7 排除高杠杆样本营收同比大于 0 排除萎缩企业。实际使用中阈值应该根据项目风险偏好调整激进策略可以把资产负债率放宽到 0.85保守策略可以要求连续两年营收正增长。这一步的作用是把几千家企业压到几十上百家让业务人员有精力去做更深度的调研。财务数据包本身解决不了“哪家企业真的值得投”它能做的是快速告诉你“哪些企业从数据上看还没出问题”把人的精力集中到真正需要判断的地方。5. 数据包里的 5 个避坑现场现象、原因、解决5.1 金额单位不统一同一列里万元与元并存现象把“营业收入”列做排序头部突然出现一批营收只有几百块的“小企业”单个指标和公司体量明显不匹配。原因源表在采集时没有统一口径早期批次按“元”导出后期批次按“万元”导出合表之后同一列里混了两种量纲。解决先看列的中位数如果中位数在千元以下基本可以判断单位是“万元”整体乘以 10000 转回“元”。med df_all[营业收入].dropna().median() if med 1000: df_all[营业收入] df_all[营业收入] * 10000 df_all[营业收入] pd.to_numeric(df_all[营业收入], errorscoerce)用中位数而不是均值判断是因为财务数据分布极度右偏头部几家大企业会把均值抬高但中位数对异常值不敏感能更真实反映多数样本的量级。errorscoerce会把非数值内容转成 NaN等于顺手清理掉“--”之类的脏值。5.2 空值以“--”或“-9999”填充统计指标直接崩现象计算净利润均值时出现一个极端负数或者某些企业净利润显示为负 9999导致整组统计量失真。原因数据导出时用特殊字符串或代码值代替缺失比如缺失填“--”、异常填“-9999”数据读取时这些值被当成普通字符串或数值处理。解决统一替换成 NaN再转成数值类型。df_all[净利润] df_all[净利润].astype(str) df_all[净利润] df_all[净利润].replace({--: pd.NA, None: pd.NA, -9999: pd.NA}) df_all[净利润] pd.to_numeric(df_all[净利润], errorscoerce)先把列转成字符串再替换比直接替换更稳因为原始数据里这些占位值可能以不同形式出现有的是字符串有的是数字。替换顺序也有讲究先转字符串再 replace再 to_numeric这样三步走任何非常规的内容都不会漏掉。处理完以后要重新看一次缺失率确认 NaN 占比是否在可接受范围内。5.3 三季报混进年度数据年末对比失真现象某企业“总资产”在 2019 年还是 1 亿级别2020 年突然变成 4 亿同比计算时出现异常跳变。原因文件里混入的不是年报数据而是三季报或半年报数据不同报告期的资产规模天然不具备可比性。解决看数据里是否有“报告期”或“季度”字段如果存在先筛选出 Q4 或年报记录再计算。if 报告期 in df_all.columns: df_annual df_all[df_all[报告期].astype(str).str.contains(年报|Q4|12-31, naFalse)] else: df_annual df_all[df_all[统计年度].astype(str).str.contains(12-31, naFalse)]这里的处理逻辑是“能精确就精确不能精确就按日期尾串筛选”。字段名每个版本不一样我先确认字段存在性再执行避免报错。筛选之后还要复查一遍年份跨度确认数据分布没有因为筛选产生新的断层。5.4 企业改名后重复统计名称清洗只能救一半现象同一家公司在表里出现两条记录但名称差了几个字按“企业名称”去重对不上。原因企业进行过工商变更名称从“某科技有限公司”改成“某智能科技股份有限公司”两个批次各采了一个版本。解决如果数据里有统一社会信用代码或注册号优先用它做去重如果没有只能靠“去掉后缀 保留核心字号”的方式做近似匹配。df_all[核心标识] ( df_all[企业名称] .astype(str) .str.replace(r.*?|\(.*?\), , regexTrue) .str.replace(股份有限公司|有限责任公司|有限公司, , regexTrue) .str.strip() )这段正则会把括号里的地区信息去掉再把公司后缀去掉留下“某智能科技”这样的核心字号。但这段代码不能保证百分百准确两家不同企业可能共享核心字号所以它只适合用来“标记疑似重复”不能直接做删除依据。正确流程是先删除完全重复主键的行再对疑似重复名单做人工抽查。5.5 表尾多出“合计”和“平均”汇总行现象统计营收分布时最大值高到离谱或者某个行业合计金额远超十家头部企业的总和。原因CSV 文件末尾自带合计行和平均行读表时被当成普通企业样本。解决按企业名称列过滤掉包含“合计”“平均”“总计”字样的行。summary_keywords [合计, 平均, 总计, 小计] mask df_all[企业名称].astype(str).str.contains(|.join(summary_keywords), naFalse) df_all df_all[~mask]naFalse很关键它能保证企业名称为空的行不会被 str.contains 误判删除也不会报错。过滤时机放在字段统一之后、主键检查之前因为汇总行也会参与主键构造如果晚一步处理主键里会出现“合计_2019”这种脏记录。过滤完之后最好重新打印一次行数和各年度分布确认没有把正常企业误伤。6. 进阶用法把这份数据做成“企业可用性档案”数据清洗和指标计算完成之后最后一步我建议做一张“企业可用性档案”用结构化字段记录每家企业有多少年财务记录、最近一次财务年度是哪一年、最近年度营收和净利润各是多少。这张档案表的价值在于不需要每次分析都重新读原始数据也不需要反复跑 groupby业务侧要一份名单时直接基于档案做一次简单过滤就有结果。def build_profile(g: pd.DataFrame) - pd.Series: g g.sort_values(统计年度) last g.iloc[-1] return pd.Series({ 年数: g[统计年度].nunique(), 末次年度: last[统计年度], 最近营收: last[营业收入], 最近净利润: last[净利润], 最近资产负债率: last[资产负债率], }) profile df_all.groupby(企业标识).apply(build_profile) profile[活跃池] ( (profile[年数] 3) (profile[末次年度].astype(int) 2023) )groupby之后apply返回的 DataFrame 会以“企业标识”为索引每行是一个企业的完整档案。活跃池这一列的含义是财务记录至少三年、且最近一次财务年度在 2023 年及以后。满足条件的企业可以进入持续观察名单不满足的只能作为历史样本或补充参考。这份档案还可以再做一层简单验证抽查三到五家企业把档案里的“最近营收”和原始批次表里的对应记录做人工比对。我通常在项目开始时花十分钟做这件小事它能确认前面所有清洗逻辑没有把某个字段值改错。很多人觉得数据清洗做完就结束了实际上最危险的是做完后从不验证。以前我在一次模拟项目里跳过验证直接用合并后的表建模结果发现“最近营收”列里有几千个值被单位换算翻了一万倍模型效果离谱到没法向业务解释。后来学到的教训是数据包的完整与否永远要用“抽查原始文件”来验证光看统计特征看不出这种错误。现在无论多赶我都会在交付前留一道人工核对步骤。希望这个习惯和这套处理流程能帮你在拿到类似压缩包时少走一点弯路。本文还有配套的精品资源点击获取