feat: 因果链三层验证机制(P2) — 数据验证+人工确认+状态机

- kpi_causality 加列: source_type/verify_status/verified_at/verified_by/entity_id(回填)
- 核心服务: app/services/causality_verification.py (Pearson+滞后对齐+状态机)
- 数据验证脚本: scripts/correlation-check.py (月度cron, 输出JSON报告)
- API: create/update支持source_type, GET /verify-status, PUT /{id}/verify(人工确认)
- 全部端点按entity_id账套隔离, kpi/{id}/network/simulate补跨企业校验
- 前端: KPIDetail因果链页显示验证状态徽标(数据证实/存疑/待检)
- 测试: test_causality_verification.py 37用例 + 原因果链测试全过(59个)
- 50条因果链首轮验证: 1数据证实(#37渠补率到净利润lag1 r=-0.89), 4存疑, 45待检(数据不足)
This commit is contained in:
Hermes CI Fix
2026-08-27 15:51:19 +08:00
parent 6b479bfe7d
commit fdc42d443d
10 changed files with 1063 additions and 22 deletions
@@ -0,0 +1,95 @@
"""kpi_causality 因果链验证机制迁移脚本 (2026-08-27 P2)
加列:
- source_type: varchar(20) 建链来源 AI_suggested/manual/imported
- verify_status: varchar(20) 验证状态 pending/data_verified/human_verified/disputed
- verified_at: datetime 验证时间
- verified_by: varchar(50) 验证人/AI/脚本
- entity_id: int 多租户隔离 (2026-08-27 收官补齐)
幂等: 列已存在则跳过; entity_id 回填只更新 NULL/0 行。
用法: python scripts/migrate_causality_verification.py
"""
import logging
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from sqlalchemy import text
from app.database import get_engine
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
logger = logging.getLogger("migrate-causality-verification")
COLUMNS = [
("source_type", "ALTER TABLE kpi_causality ADD COLUMN source_type VARCHAR(20) NOT NULL DEFAULT 'manual' COMMENT '建链来源 AI_suggested/manual/imported'"),
("verify_status", "ALTER TABLE kpi_causality ADD COLUMN verify_status VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT '验证状态 pending/data_verified/human_verified/disputed'"),
("verified_at", "ALTER TABLE kpi_causality ADD COLUMN verified_at DATETIME NULL COMMENT '验证时间'"),
("verified_by", "ALTER TABLE kpi_causality ADD COLUMN verified_by VARCHAR(50) NULL COMMENT '验证人/AI/脚本'"),
("entity_id", "ALTER TABLE kpi_causality ADD COLUMN entity_id INT NOT NULL DEFAULT 1 COMMENT '企业ID (多租户隔离 2026-08-27)'"),
]
def run():
engine = get_engine()
with engine.connect() as conn:
# 1. 检查表是否存在
exists = conn.execute(text(
"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'kpi_causality'"
)).scalar()
if not exists:
logger.error("kpi_causality 表不存在,跳过")
return 1
# 2. 现有列
existing = {r[0] for r in conn.execute(text(
"SELECT column_name FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'kpi_causality'"
))}
logger.info("现有列: %s", sorted(existing))
# 3. 加列(幂等)
for col, ddl in COLUMNS:
if col in existing:
logger.info("%s 已存在,跳过", col)
else:
conn.execute(text(ddl))
logger.info("已添加列 %s", col)
# 4. 回填 entity_id(无条件从 source KPI 对齐,纠正默认值偏差)
# 仅当来源KPI存在才回填;无来源KPI的孤儿链保持原值
conn.execute(text(
"UPDATE kpi_causality c JOIN kpi_definitions k ON k.id = c.source_kpi_id "
"SET c.entity_id = k.entity_id"
))
orphan = conn.execute(text(
"SELECT COUNT(*) FROM kpi_causality c LEFT JOIN kpi_definitions k ON k.id = c.source_kpi_id "
"WHERE k.id IS NULL"
)).scalar()
if orphan:
logger.warning("%d 条因果链无来源KPI(孤儿链)", orphan)
else:
logger.info("entity_id 已全部按来源KPI回填")
# 5. 验证
cols = {r[0] for r in conn.execute(text(
"SELECT column_name FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'kpi_causality'"
))}
missing = {c for c, _ in COLUMNS} - cols
if missing:
logger.error("仍有缺失列: %s", missing)
return 1
row = conn.execute(text(
"SELECT COUNT(*) FROM kpi_causality WHERE entity_id IS NULL OR entity_id = 0"
)).scalar()
if row:
logger.error("仍有 %d 行 entity_id 为空", row)
return 1
total = conn.execute(text("SELECT COUNT(*) FROM kpi_causality")).scalar()
logger.info("迁移完成: kpi_causality %d 条, 新列: source_type/verify_status/verified_at/verified_by/entity_id", total)
return 0
if __name__ == "__main__":
sys.exit(run())