- 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待检(数据不足)
96 lines
4.0 KiB
Python
96 lines
4.0 KiB
Python
"""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())
|