Files
report-admin/backend/app/api/era2_state.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

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()