05-数据质量与校验规则

akshare 聚合抓取的数据存在缺失、异常、漂移等风险。数据写入前后需经多层校验,确保本地库数据可信。本规则定义校验维度、阈值与处理方式。

校验维度

数据质量按五个维度把控:

  1. 完整性:该有的数据是否有(行级、列级)
  2. 准确性:数值是否在合理范围
  3. 一致性:表与表之间、字段与字段之间是否矛盾
  4. 时效性:数据是否最新,是否有非预期延迟
  5. 交叉验证:同一数据点从多个独立来源获取,比对一致性

完整性校验

行级完整性

检查每个标的的数据是否覆盖预期时间范围:

-- 校验指数日线覆盖范围
SELECT
    d.index_code,
    d.index_name,
    MIN(t.trade_date) AS first_date,
    MAX(t.trade_date) AS last_date,
    COUNT(*) AS row_count,
    -- 交易日数估算(排除周末,粗略)
    (julianday(MAX(t.trade_date)) - julianday(MIN(t.trade_date))) * 250 / 365 AS expected_rows,
    CASE
        WHEN COUNT(*) < (julianday(MAX(t.trade_date)) - julianday(MIN(t.trade_date))) * 200 / 365
        THEN 'SUSPICIOUS'
        ELSE 'OK'
    END AS status
FROM dim_index d
LEFT JOIN index_daily t ON d.index_code = t.index_code
GROUP BY d.index_code
HAVING status = 'SUSPICIOUS';

阈值说明:

  • 250/365 是全年交易日占比,200/365 是宽松下限(容忍 20% 缺失)
  • 新上市标的可能不足一年,按实际交易日数校验
  • 校验失败的标的写入 update_failure_log,标记 integrity_row

列级完整性

关键字段不允许缺失:

不允许 NULL 的字段
index_dailyclose, trade_date
fund_dailyclose, trade_date
stock_dailyclose, trade_date
index_valuationpe 或 pe_ttm 至少一个
stock_financialrevenue, net_profit
macro_indicatorvalue
-- 检查指数日线关键字段缺失
SELECT index_code, COUNT(*) AS missing_close
FROM index_daily
WHERE close IS NULL
GROUP BY index_code
HAVING missing_close > 0;

交易日缺失检测

对比交易日历,找出未更新的日期:

-- 假设有 trade_calendar 表(可从 akshare 的 tool_trade_date_hist_sina 获取)
SELECT c.trade_date
FROM trade_calendar c
LEFT JOIN index_daily t
    ON t.trade_date = c.trade_date
    AND t.index_code = '000300'
WHERE c.is_open = 1
  AND t.trade_date IS NULL
  AND c.trade_date >= date('now', '-30 days');

交易日历表结构:

CREATE TABLE IF NOT EXISTS trade_calendar (
    trade_date TEXT PRIMARY KEY,
    is_open INTEGER NOT NULL,           -- 1=交易日, 0=非交易日
    exchange TEXT NOT NULL              -- SSE/SZSE
);

准确性校验

数值范围校验

关键数值字段设定合理范围,超出范围标记异常:

字段合理范围异常处理
index_dailyopen/close/high/low(0, 100000)标记 range_error,人工核查
index_dailyhigh >= max(open, close)必须成立标记 logic_error
index_dailylow <= min(open, close)必须成立标记 logic_error
index_dailypct_change(-20, 20)超过 ±10% 记录但不阻断
stock_dailyturnover_rate[0, 100]超范围标记
index_valuationpe(0, 1000)负值或超高标记
index_valuationpe_percentile[0, 1]超范围标记
stock_financialdebt_ratio[0, 1]超范围标记
macro_indicatorCPI_YOY(-10, 20)超范围标记
-- 高低价逻辑校验
SELECT index_code, trade_date, open, close, high, low
FROM index_daily
WHERE high < GREATEST(open, close)
   OR low > LEAST(open, close)
   OR high < low;

跨表一致性

同一天同一标的的数据在不同表中应一致:

-- stock_daily 的 pe_ttm 与 stock_valuation 的 pe_ttm 应一致
SELECT s.stock_code, s.trade_date,
       s.pe_ttm AS daily_pe,
       v.pe_ttm AS val_pe,
       ABS(s.pe_ttm - v.pe_ttm) AS diff
FROM stock_daily s
JOIN stock_valuation v
    ON s.stock_code = v.stock_code
    AND s.trade_date = v.trade_date
WHERE ABS(s.pe_ttm - v.pe_ttm) > 0.1;

允许小误差(浮点精度),差异超过 0.1 标记 cross_table_inconsistency

时效性校验

数据新鲜度

检查最新数据日期是否在预期内:

SELECT 'index_daily' AS table_name, MAX(trade_date) AS latest_date,
       julianday('now') - julianday(MAX(trade_date)) AS days_behind
FROM index_daily
UNION ALL
SELECT 'fund_daily', MAX(trade_date),
       julianday('now') - julianday(MAX(trade_date))
FROM fund_daily
UNION ALL
SELECT 'stock_daily', MAX(trade_date),
       julianday('now') - julianday(MAX(trade_date))
FROM stock_daily;

阈值:

  • 日级数据:days_behind > 3(跨周末)标记 stale_data
  • 月级数据:days_behind > 35 标记
  • 季级数据:days_behind > 95 标记

更新时间戳

每张表记录最后更新时间:

SELECT key, value AS last_updated
FROM db_meta
WHERE key LIKE '%_last_updated';

update_all.py 完成每张表更新后写入 db_meta

def mark_table_updated(conn, table_name: str):
    conn.execute("""
        INSERT OR REPLACE INTO db_meta (key, value, updated_at)
        VALUES (?, ?, datetime('now'))
    """, (f"{table_name}_last_updated", datetime.now().isoformat()))

多数据源交叉验证

单一数据源存在系统性偏差或静默错误的风险(如上游接口数据源切换、复权方式不一致、口径调整等)。对关键数据点,从两个独立上游来源同时获取并比对,是发现这类问题的最有效手段。

验证范围

并非所有数据都需要交叉验证,按数据重要性分级:

优先级数据类型验证字段主源备源理由
必须指数日线close东方财富新浪估值分位计算依赖
必须指数估值pe/pb理杏仁自算(成分股加权)定投决策核心依据
必须基金净值unit_nav东方财富基金公司官网业绩跟踪基础
抽样股票日线close东方财富新浪每 20 天抽样 1 天
抽样行业日线close新浪东方财富每 20 天抽样 1 天
不验证资金流向-单源-无独立第二来源
不验证基金持仓-单源-口径统一,无第二来源

验证策略

三种触发方式结合,平衡覆盖度与抓取成本:

  1. 全量交叉验证:首次建库或 --mode full 时,对所有历史数据做交叉验证。耗时较长但确保基线可信。
  2. 抽样交叉验证:日常 --mode daily 更新时,对新数据按比例抽样(如每 20 个交易日取 1 天,或最近 5 个交易日全量)。成本低,能发现持续性偏差。
  3. 关键时点强制验证:月末、季末、单日跌幅超 3% 的日期,强制交叉验证。这些时点数据准确性对分析结论影响最大。

容差设定

不同字段的容差按数据性质区分:

字段类型容差规则阈值
价格类(close/open/high/low)绝对差≤ 0.01 元
百分比类(PE/PB/涨跌幅)相对差≤ 1%
估值分位绝对差≤ 0.02
基金净值绝对差≤ 0.001 元
成交量/成交额相对差≤ 5%(不同源口径可能略异)

容差内的差异标记为 match,超容差标记为 mismatch,单源缺失标记为 missing

验证结果存储

交叉验证结果写入 cross_validation_log 表(schema 见 02-数据架构与表结构设计),便于追踪历史趋势:

SELECT target_code, trade_date, field_name,
       value_primary, value_secondary, diff_pct, status
FROM cross_validation_log
WHERE status = 'mismatch'
ORDER BY trade_date DESC
LIMIT 50;

数据质量标记联动

交叉验证结果影响主表的 data_quality 字段:

交叉验证结果data_quality 标记处理
两源一致(match)ok正常使用
差异超容差(mismatch)suspicious查询接口默认过滤,人工核查
两源都缺失missing重新获取或标记为缺失
仅单源有数据single_source可用但标注可信度较低

实现脚本

交叉验证逻辑在 fetcher_validation.py,由 update_all.py 在数据获取后调用:

# 简化示意:指数收盘价交叉验证
def validate_index_close(conn, run_id, index_code, symbol_em, trade_dates):
    """从东财和新浪分别获取收盘价,比对一致性。"""
    df_em = fetch_with_retry(lambda: ak.stock_zh_index_daily_em(symbol=symbol_em))()
    df_sina = fetch_with_retry(lambda: ak.stock_zh_index_daily(symbol=symbol_em))()

    for date in trade_dates:
        close_em = df_em.loc[df_em['date'] == date, 'close'].values
        close_sina = df_sina.loc[df_sina['date'] == date, 'close'].values
        if len(close_em) == 0 or len(close_sina) == 0:
            status = 'missing'
        elif abs(close_em[0] - close_sina[0]) <= 0.01:
            status = 'match'
        else:
            status = 'mismatch'
            # 标记主表数据为可疑
            conn.execute(
                "UPDATE index_daily SET data_quality='suspicious' "
                "WHERE index_code=? AND trade_date=?",
                (index_code, date)
            )
        # 写入验证日志
        conn.execute(
            "INSERT INTO cross_validation_log (...) VALUES (...)",
            (run_id, index_code, date, 'close', 'em', 'sina',
             close_em[0] if len(close_em) else None,
             close_sina[0] if len(close_sina) else None, ...)
        )

完整实现见 02-数据脚本/fetcher_validation.py

校验执行

校验脚本

校验逻辑封装在 update_all.pyvalidate 模式:

python update_all.py --mode validate

输出校验报告到 logs/validation_YYYYMMDD.log,包含:

  • 各表的行数、日期范围
  • 异常记录数与详情
  • 跨表不一致记录
  • 时效性状态

校验结果表

校验结果写入 validation_result 表,便于查询历史趋势:

CREATE TABLE IF NOT EXISTS validation_result (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    run_id TEXT NOT NULL,
    table_name TEXT NOT NULL,
    check_type TEXT NOT NULL,           -- integrity/accuracy/consistency/timeliness
    check_name TEXT NOT NULL,           -- 具体检查项
    status TEXT NOT NULL,               -- pass/warning/fail
    detail TEXT,                        -- 详情(异常数量、样本)
    checked_at TEXT NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_validation_run
    ON validation_result(run_id);

阈值与告警

检查项passwarningfail
行级完整性(缺失比例)<5%5-15%>15%
列级完整性(关键字段缺失)01-10>10
数值范围异常01-20>20
跨表不一致01-50>50
日级数据新鲜度(天)≤12-3>3

fail 级别需要立即处理,warning 级别记录但可继续。

异常处理流程

发现异常后的处理顺序:

  1. 记录:写入 validation_resultupdate_failure_log
  2. 隔离:异常数据不删除,但在查询接口中默认过滤(通过 is_valid 标记或视图)
  3. 排查:人工核查异常样本,判断是 akshare 接口问题还是真实数据
  4. 修复
    • 接口问题:切换备用接口或升级 akshare
    • 真实异常:保留数据但标注
    • 数据错误:用正确数据覆盖(需记录修改原因)
  5. 复盘:同类异常加入校验规则,避免下次遗漏

数据标记字段

为支持异常隔离,关键表增加 data_quality 字段:

-- 以 index_daily 为例,其他表同理
ALTER TABLE index_daily ADD COLUMN data_quality TEXT DEFAULT 'ok';
-- 取值:ok / suspicious / error / manual_fixed

查询接口默认只返回 data_quality IN ('ok', 'manual_fixed') 的数据,异常数据需显式查询:

-- 默认查询(只看正常数据)
SELECT * FROM index_daily
WHERE index_code = '000300'
  AND data_quality IN ('ok', 'manual_fixed')
ORDER BY trade_date;

-- 排查时查看异常数据
SELECT * FROM index_daily
WHERE data_quality NOT IN ('ok', 'manual_fixed');

这样设计避免异常数据污染分析结论,同时保留数据用于排查。

定期全量校验

日常 validate 模式只校验近期数据(近 30 天),每月一次全量校验:

python update_all.py --mode validate --full

全量校验覆盖所有历史数据,耗时较长(可能数十分钟),安排在周末执行。全量校验的重点:

  • 历史数据的完整性(是否有早期缺失未发现)
  • 跨年数据的一致性(除权除息后复权数据是否正确)
  • 长期未更新的标的(可能已退市)