Initial commit
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
commit
71db82393a
@@ -0,0 +1,246 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user