JerBouma / 数据质量
FinanceDatabase 数据质量审计:八个边界与实测证据
它不是一个选股器,而是一张标的黄页。这页把「用之前必须先知道」的八个边界逐条配上 2026-09-29 的实测证据:哪些列大量为空、同一家公司为什么会重复出现、symbol 为什么用 df["symbol"] 取不到、哪些写法不报错却会给出全表。看完再决定这份清单能不能进你的研究流程。
事实核验卡(核验日期 2026-09-29)
- FinanceDatabase 版本
- 2.4.0(PyPI 与 GitHub Release,2026-06-02 发布)
- Python 版本
- 3.11.9(本站实测);包要求 ≥3.10, <3.16
- 数据库快照
- GitHub main 分支 compression/*.bz2(main 最后提交 2026-09-27)
- 核验日期
- 2026-09-29
- Equities 行数
- 112,718 行 × 21 列
- ETF 行数
- 36,481 行 × 9 列
- 数据来源
- raw.githubusercontent.com/JerBouma/FinanceDatabase/main/compression/
- 最后验证时间
- 2026-09-29(本机 venv 实跑,输出已归档)
以上数字来自本站 2026-09-29 在 Windows + Python 3.11.9 + financedatabase 2.4.0 下的真实运行输出,不是从文章里转抄的;数据文件由上游持续更新,行数会变,以官方 PyPI 与 GitHub Release 为准。采集时 GitHub star 约 9,374(2026-09-29)。
八个数据边界:先把这份清单读完再动手
每条都对应一个实测数字。这些不是「注意事项」,而是会直接改变你结论的东西。
| # | 边界 | 实测证据(2026-09-29) | 你该怎么做 |
|---|---|---|---|
| 1 | 同一家公司会在多个交易所重复出现 | 全库 65,317 行与其它行同名,涉及 19,633 个名字 | 先按 micro/exchange 锁市场,再按业务规则去重 |
| 2 | symbol 是索引,不是列 | 写 df["symbol"] 或 search(symbol=...) 都不会得到你要的结果 | 用 df.index;搜索用 search(index=...) |
| 3 | market_cap 是档位标签,不是市值数字 | 取值只有 Nano/Small/Micro/Mid/Large/Mega + Cap 六种 | 需要数值市值就另接数据源,别在这一列上排序 |
| 4 | 标识符大量为空 | isin 缺失 73.00%、cusip 缺失 75.79%、figi 缺失 48.05% | ISIN 只能作为辅助键,空值不会被匹配上 |
| 5 | 分类字段缺失严重 | industry 缺失 35.54%、market_cap 缺失 32.26% | 用它们筛选前先跑一次 isna().mean() |
| 6 | 退市只用一个布尔值表示 | delisted 全库 10,404 行为 True;默认查询已排除它们 | 分不清「退市」与「长期停牌」,需结合行情侧判断 |
| 7 | 没有行情、财报与实时数据 | 定位为证券清单与元数据;官方明说不提供最新基本面与股价 | 下游接 yfinance、AKShare 或 FinanceToolkit 等 |
| 8 | 筛选结果不等于投资结论 | 按行业筛出的是「属于该行业分类的标的」,不含估值、盈利质量、流动性 | 把筛选看作建池的起点,而不是结论 |
重复上市实测:同名记录到底有多少?
「去重」这件事在这里不是可选项。下面是从 Equities 全表统计出来的实测结果。
| 指标 | 实测值 | 说明 |
|---|---|---|
| Equities 总行数 | 112,718 | 含退市,索引可取(代码本身不重复) |
| 存在同名记录的行数 | 65,317 | 占全表的 57.9% |
| 涉及的名称个数 | 19,633 | 这些名字都出现了不只一次 |
| 同名次数最多的名字 | ISHARES(257 行) | 注意:这 257 行的国家字段全都写着 Brazil |
| 索引为空的行 | 1 行 | 数量极小,但取索引时要防 NaN 导致类型错误 |
| 有 ISIN 的行 | 30,429 行 | 其余 82,289 行(73.00%)该列为空 |
name 等于 ISHARES 的 257 行全部取出来看,会发现它们的 market_cap 全都写着 Mid Cap、country 全都写着 Brazil——而代码却分布在伦敦、香港、法兰克福、圣地亚哥等地。这类字段组合说明名称列不是可靠的去重键,也说明个别记录的元数据本身就有问题。要判断是不是「真重复」,至少要看代码、交易所与 ISIN 的组合;本页第五步的可复现脚本里就有对应的检查片段,直接用 FinanceDatabase 的结果做分组即可跑出来。标识符稀疏度:ISIN 只有 9,406 个不同取值,能当锚点吗?
很多人想用 ISIN 做跨市场匹配。实测下来,这个思路在本库上会漏掉大部分标的。
| 标识符 | 有值的行 | 缺失率 | 不同取值个数 | 跨市场匹配可用性 |
|---|---|---|---|---|
isin | 30,429 | 73.00% | 9,406 | 偏弱:空值不参与匹配,且同一 ISIN 会对应多条记录 |
cusip | 27,286 | 75.79% | 12,946 | 弱:以北美为主,非美市场基本为空 |
figi | 58,556 | 48.05% | 58,232 | 尚可,但覆盖率仍不到一半 |
composite_figi | 60,892 | 45.98% | 41,131 | 可用于分组,注意不同取值个数少于有值行数 |
shareclass_figi | 63,990 | 43.23% | 29,164 | 与上面的 figi 配合使用 |
isin 有值的只有 9,406 个不同值,却覆盖 30,429 行——也就是说有 5,181 个 ISIN 对应多条记录。更稳的键是「代码 + 交易所」,ISIN 只用来做交叉验证。delisted 字段怎么用:默认查询已经帮你过滤了
这个字段看起来简单,但和「能不能取到行情」是两件事。
| 项 | 实测值 | 说明 |
|---|---|---|
delisted=True 的行 | 10,404 | 占 Equities 的 9.2% |
delisted=False 的行 | 102,314 | select() 的默认口径 |
| 字段取值种类 | 只有 True / False | 没有「停牌」「暂停上市」等中间状态 |
| 重置方法 | select(exclude_delisted=False) | 需要看历史全量时显式关闭过滤 |
search() 里的对应参数 | exclude_delisted | 与 select() 用法一致 |
| 与本库清单的关系 | 清单里有 ≠ 行情可取 | 实测清单第 2、3 行的代码在 yfinance 上取不到数据 |
delisted 过滤掉之后,清单里仍可能出现取不到行情的代码(退市边缘、长期停牌或上游未更新)。反过来,某些标的状态字段没标退市,但行情侧早就没有成交。判断「可交易性」要靠行情侧,不能只看这个布尔列。五步审计脚本:拿到表先跑这五段
本站 2026-09-29 的审计就是按这五步做的,可直接复制。每步都给出预期输出,便于你判断是否跑对。
import financedatabase as fd
equities = fd.Equities()
df = equities.select() # 默认排除退市,实测 (102314, 21)
# 第 1 步:规模与结构
print(df.shape, type(df).__name__) # 预期:(102314, 21) FinanceFrame
# 第 2 步:字段缺失率(决定哪些列能当筛选条件)
missing = df.isna().mean().sort_values(ascending=False)
print(missing.head(8)) # 预期:industry / market_cap 等排在前列
# 第 3 步:字段取值数量(确认筛选参数合法值)
print(df["market_cap"].value_counts(dropna=False))
print(equities.show_options(selection="market_cap")) # 预期:6 个档位
# 第 4 步:重复标的检查
base = df.index.to_series().astype(str).str.split(".", n=1).str[0]
dup = base.duplicated(keep=False)
print(dup.sum(), "行与其它行共享基础代码") # 预期:数量可观,需按业务口径再去重
# 第 5 步:标识符稀疏度(判断能否当匹配键)
for col in ["isin", "cusip", "figi"]:
print(col, round(df[col].isna().mean() * 100, 2))| 步骤 | 目的 | 预期输出 | 判读标准 |
|---|---|---|---|
| 1 规模与结构 | 确认装对版本、数据量级正常 | (102314, 21) FinanceFrame | 行数明显偏小 → 数据文件可能被截断 |
| 2 字段缺失率 | 决定哪些列能做筛选条件 | 缺失率从高到低的列列表 | 缺失率超过 30% 的列不建议当条件 |
| 3 取值与选项 | 拿到合法参数值 | 六档 market_cap 等 | 直接拿它当 select() 的参数白名单 |
| 4 重复检查 | 识别同一公司的多条记录 | 共享基础代码的行数 | 结合交易所字段判断,不要只看名称 |
| 5 标识符稀疏度 | 判断能否用 ISIN 做键 | 各标识符的缺失率 | 缺失率高时改用「代码 + 交易所」 |
industry 缺了三分之一的情况下,把「行业分布统计」当成事实来用。把五步跑完再动手,是本站使用 FinanceDatabase 的固定顺序。筛选器不是选股器:从清单到可回测标的池的八步
本库只覆盖这条链路的前几步,后面每一步都要换工具,而换工具时代码口径会变。
| 步骤 | 谁来做 | 本库能覆盖吗 | 这一步的典型失败点 |
|---|---|---|---|
| 1 分类筛选(国家/行业/市值档) | 本库 | 可以 | 取值写错会静默返回空表 |
| 2 去重(跨市场重复标的) | 本库 + 自己的规则 | 部分覆盖 | only_primary_listing 在中国标的上会反向筛选 |
| 3 过滤退市 | 本库 | 可以 | 布尔字段分不清退市与长期停牌 |
| 4 核对证券代码 | 本库 + 行情源 | 部分覆盖 | 清单里有、行情侧取不到(实测已复现) |
| 5 接入行情数据 | yfinance / AKShare / OpenBB 等 | 不覆盖 | 各源后缀与复权口径不同 |
| 6 补充基本面数据 | FinanceToolkit 等 | 不覆盖 | to_toolkit() 免 key 只有 5 年基本面数据 |
| 7 加入流动性与估值条件 | 自建规则 | 不覆盖 | 流动性指标要用行情侧数据算 |
| 8 形成可回测标的池并留档 | 自建流程 | 不覆盖 | 没有版本的池子无法复现,结论不可追溯 |
universe_20260929_fd240.csv)。这样当上游数据更新之后,你还能说清当初的结论是在哪一份快照上得出的——这正是这套数据最需要补上的那部分工程习惯。数据质量常见问题
为什么筛选结果里会有重复的股票?
因为同一家公司可能在多个交易所挂牌,本库会为每个挂牌分别建一条记录。实测全库有 65,317 行与其它行同名,涉及 19,633 个名字。要收敛,先按 exchange/mic 锁定你要的市场,再按业务规则去重;单靠名称或 only_primary_listing 都会误伤。数据以官方仓库当前版本为准。
symbol 为什么不是普通列?
它是 DataFrame 的索引,索引名就叫 symbol。所以 df["symbol"] 会报 KeyError,而 search(symbol="TSLA") 更隐蔽——它只打印一句 symbol is not a valid column.,然后返回全部 112,718 行。正确写法是 df.index 或 search(index="TSLA")。
ISIN 缺失这么严重,还能做跨市场匹配吗?
可以做辅助校验,不建议做可取匹配键。实测 isin 缺失 73.00%,仅有 9,406 个不同取值却覆盖 30,429 行;空值不参与匹配,会静默漏掉大量记录。更稳的键是「代码 + 交易所」,另外注意中国标的的 ISIN 缺失率更高(77.58%)。
market_cap 为什么不能用来算分位数?
因为它不是数字,而是 Nano/Small/Micro/Mid/Large/Mega 六档文字标签(各档后面跟一个空格与 Cap)。实测行数最多的是 Nano Cap(22,793 行),最少的是 Mega Cap(641 行),另有 32.26% 的列为空。需要真实市值数字必须另接数据源。
这份数据能替代财报或行情吗?
不能。官方在 README 里明确写出,它的目的不是提供最新的基本面或股价数据。它解决「有哪些标的」,行情与财报要交给 yfinance、AKShare、OpenBB 或 FinanceToolkit 一类工具。本站的边界页记录了 to_toolkit() 的实际调用情况。
数据多久更新一次,会不会突然变化?
数据由社区与维护者持续更新,行数会随版本变化:本站实测 Equities 112,718 行与官方 README 的统计表一致(2026-09-29 核验),但这不代表下个月还是同一个数字。写结论时请带上核验日期;以官方 PyPI 与 GitHub Release 为准。