Files
report-admin/scripts/era2/03_test_pipeline.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

195 lines
7.5 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
"""
二期 Pipeline 测试脚本
数据加载 → PCA赋分(SPSS对齐) → 标准化 → 水平判定 → 聚类 → 统计分析
不调用 LLM,不生成 HTML 报告
支持两种模式:
--district 长宁区 只分析长宁区8所学校(与一期对比验证)
--district all 分析全部13个区176所学校
"""
import sys
import time
import argparse
import json
import logging
from pathlib import Path
import numpy as np
# 添加当前目录到 path(加载 config_era2 和 data_engine_era2
sys.path.insert(0, str(Path(__file__).parent))
from data_engine_era2 import DataEngineEra2
from engines.pca_scoring_engine_era2 import PcaScoringEngineEra2
from engines.stats_engine_era2 import StatsEngineEra2 as StatsEngine
from config_era2 import DIMENSION_FRAMEWORK, LEVEL_THRESHOLDS
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
datefmt="%H:%M:%S",
)
logger = logging.getLogger(__name__)
def clean_for_json(obj):
"""处理 numpy 类型"""
if isinstance(obj, dict):
return {k: clean_for_json(v) for k, v in obj.items()}
elif isinstance(obj, list):
return [clean_for_json(v) for v in obj]
elif isinstance(obj, (np.integer,)):
return int(obj)
elif isinstance(obj, (np.floating,)):
return round(float(obj), 4)
elif isinstance(obj, np.ndarray):
return obj.tolist()
elif isinstance(obj, float):
return round(obj, 4)
return obj
def main():
parser = argparse.ArgumentParser(description="二期数据Pipeline测试")
parser.add_argument("--district", type=str, default="长宁区",
help="区域筛选,'all'表示全部区域,默认'长宁区'")
parser.add_argument("--school", type=str, default=None,
help="指定一所学校查看详情")
parser.add_argument("--list-schools", action="store_true",
help="列出所有可用学校")
args = parser.parse_args()
start_time = time.time()
print("=" * 70)
print("📊 二期数据 Pipeline 测试")
print("=" * 70)
# ===== Step 1: 加载数据 =====
district = None if args.district == "all" else args.district
logger.info(f"[1/5] 加载二期数据... (区域: {args.district})")
data_engine = DataEngineEra2(district_filter=district)
data_engine.load_all()
summary = data_engine.summary()
print(f"\n📋 数据摘要:")
print(f" 学校数: {summary['school_count']}")
print(f" 基础信息行数: {summary['basic_info_rows']}")
print(f" 课程实施行数: {summary['course_impl_rows']}")
print(f" 学科课程行数: {summary['subject_impl_rows']}")
if args.list_schools:
print(f"\n可用学校 ({len(data_engine.schools)}所):")
for i, s in enumerate(data_engine.schools, 1):
print(f" {i:3d}. {s}")
return
# ===== Step 2: PCA赋分(SPSS对齐) =====
logger.info(f"[2/5] PCA赋分计算 ({len(data_engine.schools)}所学校, SPSS对齐)...")
pca_engine = PcaScoringEngineEra2(data_engine)
pca_sub_scores = pca_engine.compute_all()
# 打印PCA赋分摘要
print(f"\n📊 PCA赋分结果摘要 (前5所学校):")
for school in list(data_engine.schools)[:5]:
if school in pca_sub_scores.index:
row = pca_sub_scores.loc[school]
vals = [f"{c}:{row[c]:.1f}" for c in row.index[:4] if not np.isnan(row[c])]
print(f" {school}: {', '.join(vals)}...")
# ===== Step 3: 标准化 =====
logger.info("[3/5] 全市基准标准化...")
stats_engine = StatsEngine()
sub_scores = stats_engine.compute_dimension_scores_pca(pca_sub_scores)
dim_scores = stats_engine.compute_dimension_aggregates(sub_scores)
print(f"\n📊 三级维度标准化得分 (均值50, 标准差10):")
print(f" 形状: {sub_scores.shape}")
print(f" 均值: {sub_scores.mean().mean():.2f}")
print(f" 标准差: {sub_scores.std().mean():.2f}")
print(f"\n📊 二级维度得分 (前5所学校):")
print(dim_scores.head().round(2).to_string())
# ===== Step 4: 水平判定 =====
logger.info("[4/5] 水平判定...")
levels = stats_engine.compute_levels(sub_scores)
print(f"\n📊 水平分布统计:")
for dim in levels.columns:
dist = levels[dim].value_counts().sort_index()
dist_str = " ".join([f"水平{k}:{v}所" for k, v in dist.items()])
print(f" {dim}: {dist_str}")
# ===== Step 5: 聚类 + 相关性 =====
logger.info("[5/5] 聚类分析 + 相关性...")
dim_cols = [c for c in dim_scores.columns if c in DIMENSION_FRAMEWORK]
clusters = stats_engine.cluster_analysis(dim_scores[dim_cols])
cluster_dist = {}
for school, cluster in clusters["school_clusters"].items():
cluster_dist[cluster] = cluster_dist.get(cluster, 0) + 1
print(f"\n📊 聚类分布: {cluster_dist}")
corr = stats_engine.correlation_analysis(dim_scores)
print(f"\n📊 维度间相关性:")
print(corr.round(3).to_string())
# ===== 单校详情 =====
test_school = args.school or (data_engine.schools[0] if data_engine.schools else None)
if test_school and test_school in data_engine.schools:
print(f"\n{'─' * 70}")
print(f"🏫 {test_school} 详细报告数据")
print(f"{'─' * 70}")
report_data = stats_engine.compute_school_report_data(test_school, sub_scores, dim_scores)
print(f" 总体得分: {report_data['overall']['score']}")
print(f" 排名: {report_data['overall']['rank_in_district']}/{report_data['overall']['total_schools']}")
print(f" 聚类类型: {report_data['overall']['cluster']}")
print(f"\n 二级维度:")
for dim, data in report_data['dimensions'].items():
print(f" {dim}: {data['score']} (区均{data['district_avg']}, 差{data['diff_district']:+.2f}) [{data['cluster']}]")
print(f"\n 三级维度:")
for sub_dim, data in report_data['sub_dimensions'].items():
print(f" {sub_dim}: {data['score']} 水平{data['level']} (区均{data['district_avg']})")
# 保存报告数据
output_dir = Path(__file__).parent.parent.parent / "output" / "era2"
output_dir.mkdir(parents=True, exist_ok=True)
report_clean = clean_for_json(report_data)
json_path = output_dir / f"{test_school}_report_data.json"
with open(json_path, "w", encoding="utf-8") as f:
json.dump(report_clean, f, ensure_ascii=False, indent=2)
print(f"\n ✅ 报告数据 → {json_path}")
# ===== 保存全量得分 =====
output_dir = Path(__file__).parent.parent.parent / "output" / "era2"
output_dir.mkdir(parents=True, exist_ok=True)
all_data = {
"district_filter": args.district,
"school_count": len(data_engine.schools),
"sub_dimension_scores": clean_for_json(sub_scores.to_dict()),
"dimension_scores": clean_for_json(dim_scores.to_dict()),
"levels": clean_for_json(levels.to_dict()),
"clusters": clean_for_json(clusters),
}
scores_path = output_dir / f"all_scores_{args.district}.json"
with open(scores_path, "w", encoding="utf-8") as f:
json.dump(all_data, f, ensure_ascii=False, indent=2)
elapsed = time.time() - start_time
print(f"\n{'=' * 70}")
print(f"✅ Pipeline测试完成! 耗时 {elapsed:.1f}s")
print(f" 区域: {args.district}")
print(f" 学校数: {len(data_engine.schools)}")
print(f" 得分文件: {scores_path}")
print(f"{'=' * 70}")
if __name__ == "__main__":
main()