Files
report-admin/scripts/era2/06_coverage_audit.py
T
lofyerandfactory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> 71db82393a Initial commit
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-07-13 15:38:41 +08:00

211 lines
8.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
覆盖率审计脚本
检查:
1. 二期ETL输出的字段名称,赋分引擎实际用到了多少
2. 赋分引擎里按关键词匹配的字段,在二期数据中能否命中
3. 每个维度的赋分数据源覆盖情况
"""
import sys
import re
import inspect
from pathlib import Path
from collections import defaultdict
import pandas as pd
import numpy as np
sys.path.insert(0, str(Path(__file__).parent))
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "backend"))
from data_engine_era2 import DataEngineEra2
from app.engines.scoring_engine import ScoringEngine
from config_era2 import DIMENSION_FRAMEWORK, SUBJECTS
def section(title):
print(f"\n{'=' * 80}")
print(f" {title}")
print(f"{'=' * 80}")
def main():
# ===== 加载二期全量数据(不分区,看全貌) =====
section("1. 加载二期全量数据")
engine = DataEngineEra2(district_filter=None)
engine.load_all()
print(f" 学校数: {len(engine.schools)}")
# 获取全量字段名称
course_df = engine._course_impl
subject_df = engine._subject_impl
basic_df = engine._basic_info
course_fields = sorted(course_df["字段名称"].dropna().unique())
subject_fields = sorted(subject_df["字段名称"].dropna().unique())
basic_fields = sorted(basic_df["字段名称"].dropna().unique())
print(f" 课程实施表字段名称数: {len(course_fields)}")
print(f" 学科课程表字段名称数: {len(subject_fields)}")
print(f" 基础信息表字段名称数: {len(basic_fields)}")
# ===== 2. 提取赋分引擎中所有用到的字段名称和关键词 =====
section("2. 赋分引擎字段引用分析")
# 从 ScoringEngine 源码中提取所有字符串常量(字段名称/关键词)
src = inspect.getsource(ScoringEngine)
# 提取所有中文字符串(字段名称)
# 匹配双引号和单引号中的中文字符串
field_refs = set()
keyword_refs = set()
# 精确字段引用:df["字段名称"] == "xxx" 或 字段名称 == field
exact_patterns = re.findall(r'["\']([^"\']*[\u4e00-\u9fff][^"\']*)["\']', src)
for p in exact_patterns:
# 跳过注释性文字
if len(p) > 30 or '' in p or '。' in p or '赋分' in p:
continue
field_refs.add(p)
# 关键词引用:str.contains("xxx")
contains_patterns = re.findall(r'\.str\.contains\(["\']([^"\']+)["\']', src)
for p in contains_patterns:
# 这些是用 | 分隔的关键词
for kw in p.split('|'):
keyword_refs.add(kw.strip())
# 其他关键词引用(in循环中的列表)
keyword_lists = re.findall(r'for (?:keyword|field|hw_type|resource) in \[([^\]]+)\]', src)
for kl in keyword_lists:
items = re.findall(r'["\']([^"\']+)["\']', kl)
for item in items:
if any('\u4e00' <= c <= '\u9fff' for c in item):
field_refs.add(item)
print(f"\n 赋分引擎中精确引用的字段名称: {len(field_refs)}个")
print(f" 赋分引擎中关键词引用: {len(keyword_refs)}个")
# ===== 3. 逐一检查精确字段在二期数据中的命中情况 =====
section("3. 精确字段匹配检查")
all_data_fields = set(course_fields) | set(subject_fields) | set(basic_fields)
matched = []
missing = []
for f in sorted(field_refs):
if f in all_data_fields:
matched.append(f)
else:
missing.append(f)
print(f"\n ✅ 命中: {len(matched)}/{len(field_refs)}")
print(f" ❌ 未命中: {len(missing)}/{len(field_refs)}")
if missing:
print(f"\n 未命中的字段(赋分引擎引用但二期数据中不存在):")
for f in missing:
# 尝试模糊匹配
fuzzy = [df for df in all_data_fields if f.replace('_', '') in df.replace('_', '') or df.replace('_', '') in f.replace('_', '')]
if fuzzy:
print(f" ❌ {f}")
print(f" → 可能对应: {fuzzy[:3]}")
else:
print(f" ❌ {f} (无近似匹配)")
# ===== 4. 关键词匹配检查 =====
section("4. 关键词匹配检查")
for kw in sorted(keyword_refs):
course_hits = course_df[course_df["字段名称"].str.contains(kw, na=False)]["字段名称"].unique()
subject_hits = subject_df[subject_df["字段名称"].str.contains(kw, na=False)]["字段名称"].unique()
total = len(course_hits) + len(subject_hits)
status = "✅" if total > 0 else "❌"
print(f" {status} '{kw}': 课程表{len(course_hits)}个, 学科表{len(subject_hits)}个")
if total == 0:
# 看看有没有相近的
all_names = list(course_fields) + list(subject_fields)
similar = [n for n in all_names if kw[:2] in n][:3]
if similar:
print(f" → 近似: {similar}")
# ===== 5. 按维度逐一检查赋分数据覆盖 =====
section("5. 按维度检查赋分数据覆盖(抽样一所学校)")
# 取一所数据较完整的学校
test_school = engine.schools[0]
print(f" 测试学校: {test_school}")
scoring = ScoringEngine(engine)
scores = scoring._score_school(test_school)
print(f"\n {'维度':<20} {'赋分项数':>8} {'均值':>8} {'是否有效':>8}")
print(f" {'─' * 50}")
for dim, vals in scores.items():
n = len(vals)
avg = np.mean(vals) if vals else 0
# 判断是否有效:是否全是默认值
is_default = (n <= 1 and abs(avg - 1.0) < 0.01) or (n <= 1 and abs(avg - 1.5) < 0.01) or (n <= 1 and abs(avg - 2.0) < 0.01) or (n <= 1 and abs(avg - 0.5) < 0.01)
status = "⚠️ 默认值" if is_default else "✅"
print(f" {dim:<20} {n:>8} {avg:>8.2f} {status:>8}")
# ===== 6. 多校抽样统计 =====
section("6. 多校统计:各维度赋分项数分布")
# 取前20所学校统计
sample_schools = engine.schools[:20]
dim_stats = defaultdict(list)
for school in sample_schools:
s = scoring._score_school(school)
for dim, vals in s.items():
dim_stats[dim].append(len(vals))
print(f"\n 抽样学校数: {len(sample_schools)}")
print(f"\n {'维度':<20} {'最小':>6} {'最大':>6} {'均值':>6} {'全为1':>8}")
print(f" {'─' * 50}")
for dim in DIMENSION_FRAMEWORK:
for sub in DIMENSION_FRAMEWORK[dim]["sub_dimensions"]:
vals = dim_stats.get(sub, [0])
min_v = min(vals)
max_v = max(vals)
avg_v = np.mean(vals)
all_one = sum(1 for v in vals if v <= 1)
warn = "⚠️" if all_one > len(vals) * 0.5 else ""
print(f" {sub:<20} {min_v:>6} {max_v:>6} {avg_v:>6.1f} {all_one:>4}/{len(vals)} {warn}")
# ===== 7. 二期新增但赋分引擎未使用的字段 =====
section("7. 二期数据中存在但赋分引擎未引用的高频字段(Top 30)")
# 统计二期中每个字段名称出现的学校数
course_field_school_count = course_df.groupby("字段名称")["学校名称"].nunique().sort_values(ascending=False)
subject_field_school_count = subject_df.groupby("字段名称")["学校名称"].nunique().sort_values(ascending=False)
# 过滤掉已被赋分引擎引用的
unused_course = course_field_school_count[~course_field_school_count.index.isin(field_refs)]
unused_subject = subject_field_school_count[~subject_field_school_count.index.isin(field_refs)]
# 进一步过滤:去掉被关键词匹配可能命中的
def is_keyword_matched(field_name):
for kw in keyword_refs:
if kw in str(field_name):
return True
return False
unused_course_strict = unused_course[~unused_course.index.map(is_keyword_matched)]
unused_subject_strict = unused_subject[~unused_subject.index.map(is_keyword_matched)]
print(f"\n [课程实施表] 未被引用的字段 (按学校覆盖率排序, Top 20):")
for field, cnt in unused_course_strict.head(20).items():
print(f" {field}: {cnt}所学校有数据")
print(f"\n [学科课程表] 未被引用的字段 (Top 20):")
for field, cnt in unused_subject_strict.head(20).items():
print(f" {field}: {cnt}所学校有数据")
print(f"\n✅ 覆盖率审计完成!")
if __name__ == "__main__":
main()