Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
247 lines
10 KiB
Python
247 lines
10 KiB
Python
"""
|
|
Era2 全局状态:全市266校 数据引擎 + PCA赋分引擎 + 统计引擎
|
|
启动时一次性加载,后续API直接使用
|
|
"""
|
|
import logging
|
|
import time
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional
|
|
from datetime import datetime
|
|
|
|
import pandas as pd
|
|
import numpy as np
|
|
|
|
# era2 引擎路径
|
|
ERA2_SCRIPTS = Path(__file__).parent.parent.parent.parent / "scripts" / "era2"
|
|
sys.path.insert(0, str(ERA2_SCRIPTS))
|
|
|
|
from data_engine_era2 import DataEngineEra2
|
|
from engines.pca_scoring_engine_era2 import PcaScoringEngineEra2
|
|
from engines.stats_engine_era2 import StatsEngineEra2
|
|
from config_era2 import (
|
|
SCHOOL_TYPE_MAP, DIMENSION_FRAMEWORK, LEVEL_DESCRIPTIONS,
|
|
CLUSTER_CONFIG,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# 输出目录
|
|
OUTPUT_DIR = Path(__file__).parent.parent.parent.parent / "output" / "era2"
|
|
|
|
|
|
class Era2State:
|
|
"""Era2 全市数据的全局单例状态"""
|
|
|
|
def __init__(self):
|
|
self._initialized = False
|
|
self.data_engine: Optional[DataEngineEra2] = None
|
|
self.pca_engine: Optional[PcaScoringEngineEra2] = None
|
|
self.stats_engine: Optional[StatsEngineEra2] = None
|
|
self.sub_scores: Optional[pd.DataFrame] = None
|
|
self.dim_scores: Optional[pd.DataFrame] = None
|
|
self.schools: List[str] = []
|
|
self.districts: List[str] = []
|
|
self.district_schools: Dict[str, List[str]] = {}
|
|
self.school_meta: Dict[str, dict] = {}
|
|
|
|
def initialize(self):
|
|
"""启动时加载全市数据并计算分数(耗时约40-60秒)"""
|
|
if self._initialized:
|
|
return
|
|
|
|
start = time.time()
|
|
logger.info("🚀 [Era2] 加载全市数据...")
|
|
|
|
# 1. 加载全市数据
|
|
self.data_engine = DataEngineEra2(use_city_data=True)
|
|
self.data_engine.load_all()
|
|
self.schools = self.data_engine.schools
|
|
|
|
# 2. 构建区→学校映射
|
|
self.school_meta = SCHOOL_TYPE_MAP
|
|
self.district_schools = {}
|
|
for school, info in SCHOOL_TYPE_MAP.items():
|
|
district = info.get("district", "未知")
|
|
if district not in self.district_schools:
|
|
self.district_schools[district] = []
|
|
if school in self.schools:
|
|
self.district_schools[district].append(school)
|
|
self.districts = sorted(self.district_schools.keys())
|
|
|
|
# 3. PCA赋分(全市)
|
|
logger.info(f"🔢 [Era2] PCA赋分 ({len(self.schools)}校)...")
|
|
self.pca_engine = PcaScoringEngineEra2(self.data_engine)
|
|
pca_sub_scores = self.pca_engine.compute_all()
|
|
|
|
# 4. 统计引擎
|
|
self.stats_engine = StatsEngineEra2()
|
|
self.sub_scores = self.stats_engine.compute_dimension_scores_pca(pca_sub_scores)
|
|
self.dim_scores = self.stats_engine.compute_dimension_aggregates(self.sub_scores)
|
|
|
|
self._initialized = True
|
|
elapsed = time.time() - start
|
|
logger.info(f"✅ [Era2] 初始化完成: {len(self.schools)}校, {len(self.districts)}区, 耗时{elapsed:.1f}s")
|
|
|
|
def get_districts_summary(self) -> List[dict]:
|
|
"""返回所有区的摘要(中英两份报告分别计数)"""
|
|
result = []
|
|
for district in self.districts:
|
|
schools_in_d = self.district_schools.get(district, [])
|
|
# 区内平均分
|
|
if schools_in_d and self.dim_scores is not None:
|
|
valid = [s for s in schools_in_d if s in self.dim_scores.index]
|
|
avg = float(self.dim_scores.loc[valid, "总体得分"].mean()) if valid else 50.0
|
|
else:
|
|
avg = 50.0
|
|
# 已生成报告数(中文 / 英文)
|
|
district_dir = OUTPUT_DIR / district
|
|
if district_dir.exists():
|
|
report_count_zh = len(list(district_dir.glob("*_报告.html")))
|
|
report_count_en = len(list(district_dir.glob("*_report_en.html")))
|
|
else:
|
|
report_count_zh = 0
|
|
report_count_en = 0
|
|
# 任意一种语言已有视为 has_report,用于 UI 总数显示
|
|
report_count = report_count_zh
|
|
result.append({
|
|
"district": district,
|
|
"school_count": len(schools_in_d),
|
|
"avg_score": round(avg, 2),
|
|
"report_count": report_count,
|
|
"report_count_zh": report_count_zh,
|
|
"report_count_en": report_count_en,
|
|
})
|
|
return result
|
|
|
|
def get_schools_in_district(self, district: str) -> List[dict]:
|
|
"""返回某区所有学校的详细信息"""
|
|
schools_in_d = self.district_schools.get(district, [])
|
|
if not schools_in_d:
|
|
return []
|
|
|
|
# 排名(全市排名 + 区内排名)
|
|
sorted_all = self.dim_scores["总体得分"].sort_values(ascending=False)
|
|
dist_scores = self.dim_scores.loc[
|
|
[s for s in schools_in_d if s in self.dim_scores.index], "总体得分"
|
|
].sort_values(ascending=False)
|
|
|
|
result = []
|
|
for dist_rank, (school, score) in enumerate(dist_scores.items(), 1):
|
|
info = self.school_meta.get(school, {})
|
|
city_rank = int((sorted_all >= score).sum())
|
|
|
|
# 报告状态(中文 + 英文)
|
|
report_path_zh = OUTPUT_DIR / district / f"{school}_报告.html"
|
|
report_path_en = OUTPUT_DIR / district / f"{school}_report_en.html"
|
|
has_report_zh = report_path_zh.exists()
|
|
has_report_en = report_path_en.exists()
|
|
# 兼容老字段
|
|
has_report = has_report_zh or has_report_en
|
|
report_generated_at_zh = ""
|
|
report_size_zh = 0
|
|
report_generated_at_en = ""
|
|
report_size_en = 0
|
|
if has_report_zh:
|
|
stat = report_path_zh.stat()
|
|
report_size_zh = stat.st_size
|
|
report_generated_at_zh = datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %H:%M")
|
|
if has_report_en:
|
|
stat = report_path_en.stat()
|
|
report_size_en = stat.st_size
|
|
report_generated_at_en = datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %H:%M")
|
|
# 兼容老字段:优先用中文版,没有则用英文版
|
|
report_size = report_size_zh or report_size_en
|
|
report_generated_at = report_generated_at_zh or report_generated_at_en
|
|
|
|
# 聚类
|
|
dim_cols = [c for c in self.dim_scores.columns if c in DIMENSION_FRAMEWORK]
|
|
valid_schools = [s for s in schools_in_d if s in self.dim_scores.index]
|
|
cluster_result = self.stats_engine.cluster_analysis(
|
|
self.dim_scores.loc[valid_schools, dim_cols], dimension_name="总体"
|
|
)
|
|
cluster = cluster_result["school_clusters"].get(school, "")
|
|
|
|
result.append({
|
|
"name": school,
|
|
"district": district,
|
|
"type": info.get("type", ""),
|
|
"nature": info.get("nature", ""),
|
|
"area": info.get("area", ""),
|
|
"score": round(float(score), 2),
|
|
"district_rank": dist_rank,
|
|
"city_rank": city_rank,
|
|
"total_in_district": len(dist_scores),
|
|
"total_in_city": len(sorted_all),
|
|
"cluster": cluster,
|
|
# 兼容老字段
|
|
"has_report": has_report,
|
|
"report_size": report_size,
|
|
"report_generated_at": report_generated_at,
|
|
# 中英分开
|
|
"has_report_zh": has_report_zh,
|
|
"has_report_en": has_report_en,
|
|
"report_size_zh": report_size_zh,
|
|
"report_size_en": report_size_en,
|
|
"report_generated_at_zh": report_generated_at_zh,
|
|
"report_generated_at_en": report_generated_at_en,
|
|
})
|
|
return result
|
|
|
|
def get_report_data(self, school: str, district: str) -> dict:
|
|
"""生成单校报告数据包"""
|
|
district_schools = self.district_schools.get(district, [])
|
|
report_data = self.stats_engine.compute_school_report_data(
|
|
school, self.sub_scores, self.dim_scores,
|
|
district_schools=district_schools,
|
|
)
|
|
report_data["district"] = district
|
|
report_data["total_schools_in_district"] = len(district_schools)
|
|
return report_data
|
|
|
|
def get_report_history(self) -> List[dict]:
|
|
"""扫描所有已生成的报告,返回历史列表(合并中英两份,每校最多两条)"""
|
|
history = []
|
|
if not OUTPUT_DIR.exists():
|
|
return history
|
|
for district_dir in sorted(OUTPUT_DIR.iterdir()):
|
|
if not district_dir.is_dir():
|
|
continue
|
|
district = district_dir.name
|
|
|
|
# 同时收集中文 (_报告.html) 和英文 (_report_en.html) 报告
|
|
collected = []
|
|
for html_file in sorted(district_dir.glob("*_报告.html")):
|
|
school = html_file.stem.replace("_报告", "")
|
|
collected.append((school, html_file, "zh"))
|
|
for html_file in sorted(district_dir.glob("*_report_en.html")):
|
|
school = html_file.stem.replace("_report_en", "")
|
|
collected.append((school, html_file, "en"))
|
|
|
|
for school, html_file, lang in collected:
|
|
stat = html_file.stat()
|
|
json_path = district_dir / f"{school}_report_data.json"
|
|
score = None
|
|
if json_path.exists():
|
|
try:
|
|
import json
|
|
data = json.loads(json_path.read_text("utf-8"))
|
|
score = data.get("overall", {}).get("score")
|
|
except Exception:
|
|
pass
|
|
history.append({
|
|
"school": school,
|
|
"district": district,
|
|
"type": self.school_meta.get(school, {}).get("type", ""),
|
|
"score": score,
|
|
"file_size": stat.st_size,
|
|
"generated_at": datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %H:%M:%S"),
|
|
"file_name": html_file.name,
|
|
"lang": lang,
|
|
})
|
|
return history
|
|
|
|
|
|
# 全局单例
|
|
era2_state = Era2State()
|