Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
778 lines
31 KiB
Python
778 lines
31 KiB
Python
"""
|
||
报告渲染引擎:将数据+LLM文字+模板组装成最终HTML报告
|
||
"""
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
from typing import Dict, List
|
||
import numpy as np
|
||
|
||
from jinja2 import Environment, FileSystemLoader
|
||
|
||
from ..config import (
|
||
TEMPLATES_DIR, DIMENSION_FRAMEWORK, LEVEL_DESCRIPTIONS,
|
||
OUTPUT_DIR, SCHOOL_TYPE_MAP,
|
||
)
|
||
|
||
|
||
class ReportRenderer:
|
||
"""HTML报告渲染引擎"""
|
||
|
||
def __init__(self):
|
||
self.env = Environment(
|
||
loader=FileSystemLoader(str(TEMPLATES_DIR)),
|
||
autoescape=False, # 允许HTML直接渲染
|
||
)
|
||
|
||
def render(self, report_data: Dict, llm_sections: Dict[str, str]) -> str:
|
||
"""
|
||
渲染完整HTML报告
|
||
|
||
report_data: stats_engine.compute_school_report_data() 的输出
|
||
llm_sections: llm_engine.generate_report_segments() 的输出
|
||
"""
|
||
template = self.env.get_template("base.html")
|
||
|
||
# 准备模板数据
|
||
school = report_data["school"]
|
||
context = {
|
||
"school": school,
|
||
"school_info": report_data["school_info"],
|
||
"overall": self._to_namespace(report_data["overall"]),
|
||
"dimensions": {
|
||
name: self._to_namespace(data)
|
||
for name, data in report_data["dimensions"].items()
|
||
},
|
||
"sub_dimensions": {
|
||
name: self._to_namespace(data)
|
||
for name, data in report_data["sub_dimensions"].items()
|
||
},
|
||
"framework": {
|
||
name: self._to_namespace(info)
|
||
for name, info in DIMENSION_FRAMEWORK.items()
|
||
},
|
||
"llm_sections": llm_sections,
|
||
"level_descriptions": LEVEL_DESCRIPTIONS,
|
||
"generation_date": datetime.now().strftime("%Y年%m月%d日"),
|
||
|
||
# ECharts数据 — 传原始dict,由模板的tojson过滤器序列化一次
|
||
"radar_data": self._build_radar_data(report_data),
|
||
"sub_dim_chart_data": self._build_sub_dim_charts(report_data),
|
||
"score_compare_data": self._build_score_compare(report_data),
|
||
"cluster_radar_data": self._build_cluster_radar(report_data),
|
||
"correlation_data": self._build_correlation_heatmap(report_data),
|
||
"level_dist_data": self._build_level_distribution(report_data),
|
||
"school_ranking_data": self._build_school_ranking(report_data),
|
||
# 新增图表数据
|
||
"cluster_type_dist_data": self._build_cluster_type_distribution(report_data),
|
||
"cluster_line_compare_data": self._build_cluster_line_compare(report_data),
|
||
"dim_scatter_data": self._build_dim_scatter_charts(report_data),
|
||
"dim_score_bar_data": self._build_dim_score_bars(report_data),
|
||
"dim_sub_radar_data": self._build_dim_sub_radar_charts(report_data),
|
||
# 创新图表
|
||
"profile_card_data": self._build_profile_card(report_data),
|
||
"quadrant_data": self._build_quadrant_chart(report_data),
|
||
"thermometer_data": self._build_thermometer_data(report_data),
|
||
"waterfall_data": self._build_waterfall_chart(report_data),
|
||
}
|
||
|
||
return template.render(**context)
|
||
|
||
def render_to_file(self, report_data: Dict, llm_sections: Dict[str, str],
|
||
output_path: Path = None) -> Path:
|
||
"""渲染并保存到文件"""
|
||
html = self.render(report_data, llm_sections)
|
||
school = report_data["school"]
|
||
|
||
if output_path is None:
|
||
output_dir = OUTPUT_DIR
|
||
output_dir.mkdir(parents=True, exist_ok=True)
|
||
output_path = output_dir / f"{school}_报告.html"
|
||
|
||
output_path.write_text(html, encoding="utf-8")
|
||
return output_path
|
||
|
||
def _build_radar_data(self, report_data: Dict) -> Dict:
|
||
"""构建雷达图数据"""
|
||
school = report_data["school"]
|
||
dims = list(report_data["dimensions"].keys())
|
||
school_values = [report_data["dimensions"][d]["score"] for d in dims]
|
||
avg_values = [report_data["dimensions"][d]["district_avg"] for d in dims]
|
||
|
||
return {
|
||
"dimensions": dims,
|
||
"legend": [school, "区均值"],
|
||
"series": [
|
||
{"name": school, "values": school_values},
|
||
{"name": "区均值", "values": avg_values},
|
||
],
|
||
}
|
||
|
||
def _build_sub_dim_charts(self, report_data: Dict) -> Dict:
|
||
"""构建各维度的子维度柱状图数据"""
|
||
charts = {}
|
||
part_names = {
|
||
"课程领导力": "part3", "教学变革力": "part4", "学生发展指导力": "part5",
|
||
"教师发展支持力": "part6", "教育质量评估力": "part7",
|
||
"教育条件保障力": "part8", "数字化赋能力": "part9",
|
||
}
|
||
|
||
for dim_name, info in DIMENSION_FRAMEWORK.items():
|
||
part_id = part_names[dim_name]
|
||
sub_dims = info["sub_dimensions"]
|
||
|
||
categories = []
|
||
school_values = []
|
||
avg_values = []
|
||
|
||
for sd in sub_dims:
|
||
sd_data = report_data["sub_dimensions"].get(sd, {})
|
||
if sd_data:
|
||
categories.append(sd)
|
||
school_values.append(round(sd_data["score"], 2))
|
||
avg_values.append(round(sd_data["district_avg"], 2))
|
||
|
||
charts[part_id] = {
|
||
"categories": categories,
|
||
"school_values": school_values,
|
||
"avg_values": avg_values,
|
||
}
|
||
|
||
return charts
|
||
|
||
def _build_score_compare(self, report_data: Dict) -> Dict:
|
||
"""构建得分对比横向条形图数据:本校 vs 区均值 vs 同类学校均值"""
|
||
school = report_data["school"]
|
||
dims = list(report_data["dimensions"].keys())
|
||
return {
|
||
"categories": dims,
|
||
"school_values": [round(report_data["dimensions"][d]["score"], 2) for d in dims],
|
||
"district_avg": [round(report_data["dimensions"][d]["district_avg"], 2) for d in dims],
|
||
"same_type_avg": [round(report_data["dimensions"][d]["same_type_avg"], 2) for d in dims],
|
||
"school_name": school,
|
||
}
|
||
|
||
def _build_cluster_radar(self, report_data: Dict) -> Dict:
|
||
"""构建聚类类型特征对比雷达图(较好类 vs 待提升类)"""
|
||
all_dim_scores = report_data.get("all_schools_dim_scores", {})
|
||
if not all_dim_scores:
|
||
return {}
|
||
|
||
# 从 overall cluster info 中提取各学校的聚类标签
|
||
school = report_data["school"]
|
||
dims = list(report_data["dimensions"].keys())
|
||
|
||
# 计算各学校的总体得分来判断聚类
|
||
school_totals = {}
|
||
for s in list(list(all_dim_scores.values())[0].keys()):
|
||
total = 0
|
||
for d in dims:
|
||
total += all_dim_scores.get(d, {}).get(s, 50)
|
||
school_totals[s] = total / len(dims)
|
||
|
||
# 二分聚类(简单按总分中位数分)
|
||
median_score = sorted(school_totals.values())[len(school_totals) // 2]
|
||
good_schools = [s for s, v in school_totals.items() if v >= median_score]
|
||
weak_schools = [s for s, v in school_totals.items() if v < median_score]
|
||
|
||
good_avgs = []
|
||
weak_avgs = []
|
||
for d in dims:
|
||
dim_data = all_dim_scores.get(d, {})
|
||
good_avgs.append(round(sum(dim_data.get(s, 50) for s in good_schools) / max(len(good_schools), 1), 2))
|
||
weak_avgs.append(round(sum(dim_data.get(s, 50) for s in weak_schools) / max(len(weak_schools), 1), 2))
|
||
|
||
return {
|
||
"dimensions": dims,
|
||
"legend": ["课程实施较好类", "课程实施待提升类", school],
|
||
"series": [
|
||
{"name": "课程实施较好类", "values": good_avgs},
|
||
{"name": "课程实施待提升类", "values": weak_avgs},
|
||
{"name": school, "values": [round(report_data["dimensions"][d]["score"], 2) for d in dims]},
|
||
],
|
||
"good_count": len(good_schools),
|
||
"weak_count": len(weak_schools),
|
||
}
|
||
|
||
def _build_correlation_heatmap(self, report_data: Dict) -> Dict:
|
||
"""构建维度间相关性热力图"""
|
||
correlation = report_data.get("correlation", {})
|
||
if not correlation:
|
||
return {}
|
||
|
||
dims = list(correlation.keys())
|
||
# 构建二维数组 [x_index, y_index, value]
|
||
data = []
|
||
for i, d1 in enumerate(dims):
|
||
for j, d2 in enumerate(dims):
|
||
val = correlation.get(d1, {}).get(d2, 0)
|
||
data.append([i, j, round(val, 3) if val is not None else 0])
|
||
|
||
# 短名
|
||
short_names = [d.replace("力", "").replace("教育", "") for d in dims]
|
||
|
||
return {
|
||
"dimensions": dims,
|
||
"short_names": short_names,
|
||
"data": data,
|
||
}
|
||
|
||
def _build_level_distribution(self, report_data: Dict) -> Dict:
|
||
"""构建各三级维度水平分布堆叠条形图"""
|
||
charts = {}
|
||
part_names = {
|
||
"课程领导力": "part3", "教学变革力": "part4", "学生发展指导力": "part5",
|
||
"教师发展支持力": "part6", "教育质量评估力": "part7",
|
||
"教育条件保障力": "part8", "数字化赋能力": "part9",
|
||
}
|
||
|
||
for dim_name, info in DIMENSION_FRAMEWORK.items():
|
||
part_id = part_names[dim_name]
|
||
sub_dims = info["sub_dimensions"]
|
||
|
||
categories = []
|
||
level1_pcts = []
|
||
level2_pcts = []
|
||
level3_pcts = []
|
||
level4_pcts = []
|
||
school_levels = []
|
||
|
||
for sd in sub_dims:
|
||
sd_data = report_data["sub_dimensions"].get(sd, {})
|
||
if not sd_data:
|
||
continue
|
||
dist = sd_data.get("level_distribution", {})
|
||
total = sum(dist.values())
|
||
if total == 0:
|
||
continue
|
||
|
||
categories.append(sd)
|
||
level1_pcts.append(round(dist.get("水平1", 0) / total * 100, 1))
|
||
level2_pcts.append(round(dist.get("水平2", 0) / total * 100, 1))
|
||
level3_pcts.append(round(dist.get("水平3", 0) / total * 100, 1))
|
||
level4_pcts.append(round(dist.get("水平4", 0) / total * 100, 1))
|
||
school_levels.append(sd_data.get("level", 0))
|
||
|
||
charts[part_id] = {
|
||
"categories": categories,
|
||
"level1": level1_pcts,
|
||
"level2": level2_pcts,
|
||
"level3": level3_pcts,
|
||
"level4": level4_pcts,
|
||
"school_levels": school_levels,
|
||
}
|
||
|
||
return charts
|
||
|
||
def _build_school_ranking(self, report_data: Dict) -> Dict:
|
||
"""构建区内各校维度排名对比图"""
|
||
school = report_data["school"]
|
||
all_dim_scores = report_data.get("all_schools_dim_scores", {})
|
||
dims = list(report_data["dimensions"].keys())
|
||
|
||
if not all_dim_scores:
|
||
return {}
|
||
|
||
# 获取所有学校名
|
||
first_dim = list(all_dim_scores.values())[0] if all_dim_scores else {}
|
||
all_schools = list(first_dim.keys())
|
||
|
||
# 计算每校总体得分并排序
|
||
school_totals = {}
|
||
for s in all_schools:
|
||
total = sum(all_dim_scores.get(d, {}).get(s, 50) for d in dims)
|
||
school_totals[s] = round(total / len(dims), 2)
|
||
|
||
sorted_schools = sorted(school_totals.items(), key=lambda x: x[1], reverse=True)
|
||
|
||
return {
|
||
"schools": [s[0] for s in sorted_schools],
|
||
"total_scores": [s[1] for s in sorted_schools],
|
||
"current_school": school,
|
||
"dimensions": dims,
|
||
"dim_scores": {
|
||
d: [round(all_dim_scores.get(d, {}).get(s[0], 50), 2) for s in sorted_schools]
|
||
for d in dims
|
||
},
|
||
}
|
||
|
||
def _build_cluster_type_distribution(self, report_data: Dict) -> Dict:
|
||
"""
|
||
构建课程实施类型分布饼/条形图数据(如参考报告图2-2)
|
||
展示 较好类 vs 待提升类 在区内各校的分布
|
||
"""
|
||
all_dim_scores = report_data.get("all_schools_dim_scores", {})
|
||
if not all_dim_scores:
|
||
return {}
|
||
|
||
school = report_data["school"]
|
||
dims = list(report_data["dimensions"].keys())
|
||
|
||
# 计算各学校总体得分并二分聚类
|
||
school_totals = {}
|
||
first_dim_data = list(all_dim_scores.values())[0] if all_dim_scores else {}
|
||
all_schools = list(first_dim_data.keys())
|
||
|
||
for s in all_schools:
|
||
total = sum(all_dim_scores.get(d, {}).get(s, 50) for d in dims)
|
||
school_totals[s] = total / len(dims)
|
||
|
||
median_score = sorted(school_totals.values())[len(school_totals) // 2]
|
||
good_schools = [s for s, v in school_totals.items() if v >= median_score]
|
||
weak_schools = [s for s, v in school_totals.items() if v < median_score]
|
||
|
||
school_cluster = "较好" if school in good_schools else "待提升"
|
||
|
||
return {
|
||
"good_count": len(good_schools),
|
||
"weak_count": len(weak_schools),
|
||
"total": len(all_schools),
|
||
"school_cluster": school_cluster,
|
||
"school_name": school,
|
||
"good_schools": good_schools,
|
||
"weak_schools": weak_schools,
|
||
}
|
||
|
||
def _build_cluster_line_compare(self, report_data: Dict) -> Dict:
|
||
"""
|
||
构建两类学校特征折线对比图(如参考报告图2-3)
|
||
两条折线:较好类 vs 待提升类在7个维度上的得分
|
||
"""
|
||
all_dim_scores = report_data.get("all_schools_dim_scores", {})
|
||
if not all_dim_scores:
|
||
return {}
|
||
|
||
school = report_data["school"]
|
||
dims = list(report_data["dimensions"].keys())
|
||
|
||
# 计算聚类
|
||
first_dim_data = list(all_dim_scores.values())[0] if all_dim_scores else {}
|
||
all_schools = list(first_dim_data.keys())
|
||
|
||
school_totals = {}
|
||
for s in all_schools:
|
||
total = sum(all_dim_scores.get(d, {}).get(s, 50) for d in dims)
|
||
school_totals[s] = total / len(dims)
|
||
|
||
median_score = sorted(school_totals.values())[len(school_totals) // 2]
|
||
good_schools = [s for s, v in school_totals.items() if v >= median_score]
|
||
weak_schools = [s for s, v in school_totals.items() if v < median_score]
|
||
|
||
good_avgs = []
|
||
weak_avgs = []
|
||
for d in dims:
|
||
dim_data = all_dim_scores.get(d, {})
|
||
good_avgs.append(round(sum(dim_data.get(s, 50) for s in good_schools) / max(len(good_schools), 1), 2))
|
||
weak_avgs.append(round(sum(dim_data.get(s, 50) for s in weak_schools) / max(len(weak_schools), 1), 2))
|
||
|
||
return {
|
||
"dimensions": dims,
|
||
"good_values": good_avgs,
|
||
"weak_values": weak_avgs,
|
||
"good_count": len(good_schools),
|
||
"weak_count": len(weak_schools),
|
||
}
|
||
|
||
def _build_dim_scatter_charts(self, report_data: Dict) -> Dict:
|
||
"""
|
||
构建各二级维度的聚类散点图数据(2D / 3D)
|
||
参考报告中每个维度都有一个散点图显示所有学校的聚类分布
|
||
对于有2个子维度的 → 2D散点图
|
||
对于有3个子维度的 → 3D散点图
|
||
对于有4个子维度的 → 取前2个主成分的2D散点图
|
||
"""
|
||
all_sub_scores = report_data.get("all_schools_sub_scores", {})
|
||
if not all_sub_scores:
|
||
return {}
|
||
|
||
school = report_data["school"]
|
||
charts = {}
|
||
part_names = {
|
||
"课程领导力": "part3", "教学变革力": "part4", "学生发展指导力": "part5",
|
||
"教师发展支持力": "part6", "教育质量评估力": "part7",
|
||
"教育条件保障力": "part8", "数字化赋能力": "part9",
|
||
}
|
||
|
||
first_sub = list(all_sub_scores.values())[0] if all_sub_scores else {}
|
||
all_schools = list(first_sub.keys())
|
||
|
||
for dim_name, info in DIMENSION_FRAMEWORK.items():
|
||
part_id = part_names[dim_name]
|
||
sub_dims = info["sub_dimensions"]
|
||
n_subs = len(sub_dims)
|
||
|
||
# 获取各学校在这些子维度上的得分
|
||
school_scores = {}
|
||
for s in all_schools:
|
||
scores = []
|
||
for sd in sub_dims:
|
||
val = all_sub_scores.get(sd, {}).get(s, 50)
|
||
scores.append(round(float(val), 2))
|
||
school_scores[s] = scores
|
||
|
||
# 简单二分聚类
|
||
totals = {s: sum(v) / len(v) for s, v in school_scores.items()}
|
||
med = sorted(totals.values())[len(totals) // 2]
|
||
clusters = {s: 0 if totals[s] >= med else 1 for s in all_schools}
|
||
|
||
if n_subs == 2:
|
||
# 2D散点图
|
||
data_good = []
|
||
data_weak = []
|
||
school_point = None
|
||
|
||
for s in all_schools:
|
||
point = school_scores[s]
|
||
if s == school:
|
||
school_point = point
|
||
elif clusters[s] == 0:
|
||
data_good.append(point)
|
||
else:
|
||
data_weak.append(point)
|
||
|
||
charts[part_id] = {
|
||
"type": "2d",
|
||
"axes": sub_dims,
|
||
"good_data": data_good,
|
||
"weak_data": data_weak,
|
||
"school_point": school_point,
|
||
"school_name": school,
|
||
"dim_name": dim_name,
|
||
}
|
||
|
||
elif n_subs == 3:
|
||
# 3D散点图
|
||
data_good = []
|
||
data_weak = []
|
||
school_point = None
|
||
|
||
for s in all_schools:
|
||
point = school_scores[s]
|
||
if s == school:
|
||
school_point = point
|
||
elif clusters[s] == 0:
|
||
data_good.append(point)
|
||
else:
|
||
data_weak.append(point)
|
||
|
||
charts[part_id] = {
|
||
"type": "3d",
|
||
"axes": sub_dims,
|
||
"good_data": data_good,
|
||
"weak_data": data_weak,
|
||
"school_point": school_point,
|
||
"school_name": school,
|
||
"dim_name": dim_name,
|
||
}
|
||
|
||
elif n_subs >= 4:
|
||
# 取前两个子维度做2D散点
|
||
axes = sub_dims[:2]
|
||
data_good = []
|
||
data_weak = []
|
||
school_point = None
|
||
|
||
for s in all_schools:
|
||
point = school_scores[s][:2]
|
||
if s == school:
|
||
school_point = point
|
||
elif clusters[s] == 0:
|
||
data_good.append(point)
|
||
else:
|
||
data_weak.append(point)
|
||
|
||
charts[part_id] = {
|
||
"type": "2d",
|
||
"axes": axes,
|
||
"good_data": data_good,
|
||
"weak_data": data_weak,
|
||
"school_point": school_point,
|
||
"school_name": school,
|
||
"dim_name": dim_name,
|
||
}
|
||
|
||
return charts
|
||
|
||
def _build_dim_score_bars(self, report_data: Dict) -> Dict:
|
||
"""
|
||
构建各维度独立得分柱状图数据(如参考报告图3-1、图4-1等)
|
||
展示本校 vs 区均值 vs 同类学校均值 的对比
|
||
"""
|
||
school = report_data["school"]
|
||
charts = {}
|
||
part_names = {
|
||
"课程领导力": "part3", "教学变革力": "part4", "学生发展指导力": "part5",
|
||
"教师发展支持力": "part6", "教育质量评估力": "part7",
|
||
"教育条件保障力": "part8", "数字化赋能力": "part9",
|
||
}
|
||
|
||
for dim_name, dim_data in report_data["dimensions"].items():
|
||
part_id = part_names.get(dim_name, "")
|
||
if not part_id:
|
||
continue
|
||
|
||
charts[part_id] = {
|
||
"dim_name": dim_name,
|
||
"school_name": school,
|
||
"school_score": round(dim_data["score"], 2),
|
||
"district_avg": round(dim_data["district_avg"], 2),
|
||
"same_type_avg": round(dim_data["same_type_avg"], 2),
|
||
}
|
||
|
||
return charts
|
||
|
||
def _build_dim_sub_radar_charts(self, report_data: Dict) -> Dict:
|
||
"""
|
||
构建各二级维度的子维度雷达图(如参考报告图3-2)
|
||
多条线对比: 本校 vs 区均值 vs 同类学校均值
|
||
"""
|
||
school = report_data["school"]
|
||
all_sub_scores = report_data.get("all_schools_sub_scores", {})
|
||
charts = {}
|
||
part_names = {
|
||
"课程领导力": "part3", "教学变革力": "part4", "学生发展指导力": "part5",
|
||
"教师发展支持力": "part6", "教育质量评估力": "part7",
|
||
"教育条件保障力": "part8", "数字化赋能力": "part9",
|
||
}
|
||
|
||
# Get same-type schools
|
||
school_info = report_data.get("school_info", {})
|
||
school_type = school_info.get("type", "")
|
||
all_schools = list(list(all_sub_scores.values())[0].keys()) if all_sub_scores else []
|
||
same_type_schools = [s for s in all_schools if SCHOOL_TYPE_MAP.get(s, {}).get("type") == school_type]
|
||
|
||
for dim_name, info in DIMENSION_FRAMEWORK.items():
|
||
part_id = part_names.get(dim_name, "")
|
||
if not part_id:
|
||
continue
|
||
|
||
sub_dims = info["sub_dimensions"]
|
||
school_values = []
|
||
district_avg = []
|
||
same_type_avg = []
|
||
|
||
for sd in sub_dims:
|
||
sd_data = report_data["sub_dimensions"].get(sd, {})
|
||
if sd_data:
|
||
school_values.append(round(sd_data["score"], 2))
|
||
district_avg.append(round(sd_data["district_avg"], 2))
|
||
# 计算同类学校均值
|
||
if same_type_schools and all_sub_scores:
|
||
st_vals = [all_sub_scores.get(sd, {}).get(s, 50) for s in same_type_schools]
|
||
same_type_avg.append(round(sum(float(v) for v in st_vals) / len(st_vals), 2))
|
||
else:
|
||
same_type_avg.append(district_avg[-1])
|
||
|
||
if len(sub_dims) >= 3:
|
||
charts[part_id] = {
|
||
"type": "radar",
|
||
"sub_dims": sub_dims,
|
||
"school_name": school,
|
||
"school_values": school_values,
|
||
"district_avg": district_avg,
|
||
"same_type_avg": same_type_avg,
|
||
}
|
||
else:
|
||
charts[part_id] = {
|
||
"type": "bar",
|
||
"sub_dims": sub_dims,
|
||
"school_name": school,
|
||
"school_values": school_values,
|
||
"district_avg": district_avg,
|
||
"same_type_avg": same_type_avg,
|
||
}
|
||
|
||
return charts
|
||
|
||
# ========== 创新图表数据构建 ==========
|
||
|
||
def _build_profile_card(self, report_data: Dict) -> Dict:
|
||
"""
|
||
A. 学校画像卡(综合仪表盘)
|
||
- 总体得分环形仪表盘
|
||
- 7个维度的红绿灯状态(基于各维度下子维度的最低水平)
|
||
- 20个三级维度的水平分布概览
|
||
"""
|
||
school = report_data["school"]
|
||
overall = report_data["overall"]
|
||
|
||
# 维度红绿灯:每个二级维度取其子维度的最低水平作为"短板"指示
|
||
dim_signals = []
|
||
for dim_name, dim_data in report_data["dimensions"].items():
|
||
sub_dims = DIMENSION_FRAMEWORK[dim_name]["sub_dimensions"]
|
||
levels = []
|
||
for sd in sub_dims:
|
||
sd_data = report_data["sub_dimensions"].get(sd, {})
|
||
if sd_data:
|
||
levels.append(sd_data.get("level", 0))
|
||
min_level = min(levels) if levels else 0
|
||
avg_level = round(sum(levels) / len(levels), 1) if levels else 0
|
||
dim_signals.append({
|
||
"name": dim_name,
|
||
"score": round(dim_data["score"], 2),
|
||
"min_level": min_level,
|
||
"avg_level": avg_level,
|
||
"rank": dim_data.get("rank_in_district", 0),
|
||
})
|
||
|
||
# 20个三级维度的水平分布统计
|
||
level_counts = {1: 0, 2: 0, 3: 0, 4: 0}
|
||
for sd_name, sd_data in report_data["sub_dimensions"].items():
|
||
lv = sd_data.get("level", 0)
|
||
if lv in level_counts:
|
||
level_counts[lv] += 1
|
||
|
||
return {
|
||
"school_name": school,
|
||
"school_type": report_data["school_info"].get("type", ""),
|
||
"total_score": overall["score"],
|
||
"district_avg": overall["district_avg"],
|
||
"rank": overall["rank_in_district"],
|
||
"total_schools": overall["total_schools"],
|
||
"cluster": overall.get("cluster", ""),
|
||
"dim_signals": dim_signals,
|
||
"level_counts": level_counts,
|
||
"total_sub_dims": sum(level_counts.values()),
|
||
}
|
||
|
||
def _build_quadrant_chart(self, report_data: Dict) -> Dict:
|
||
"""
|
||
B. 优势-短板象限图(Gap Analysis)
|
||
X轴 = 得分, Y轴 = 与区均值的差值
|
||
四象限:右上=核心优势, 左下=急需改进, 右下=隐性风险, 左上=潜力项
|
||
"""
|
||
school = report_data["school"]
|
||
items = []
|
||
|
||
for sd_name, sd_data in report_data["sub_dimensions"].items():
|
||
parent_dim = sd_data.get("parent_dimension", "")
|
||
items.append({
|
||
"name": sd_name,
|
||
"parent": parent_dim,
|
||
"score": round(sd_data["score"], 2),
|
||
"diff": round(sd_data["diff_district"], 2),
|
||
"level": sd_data.get("level", 0),
|
||
})
|
||
|
||
return {
|
||
"school_name": school,
|
||
"items": items,
|
||
"x_center": 50, # 区均值(标准化后均值=50)
|
||
"y_center": 0, # 差值=0 的参照线
|
||
}
|
||
|
||
def _build_thermometer_data(self, report_data: Dict) -> Dict:
|
||
"""
|
||
C. 维度温度计条形图数据
|
||
为每个三级维度构建横向温度计数据:
|
||
- 得分范围20-80
|
||
- 水平分界线
|
||
- 本校位置、区均值位置、同类学校均值位置
|
||
"""
|
||
school = report_data["school"]
|
||
school_type = report_data["school_info"].get("type", "")
|
||
all_sub_scores = report_data.get("all_schools_sub_scores", {})
|
||
same_type_schools = [s for s in SCHOOL_TYPE_MAP
|
||
if SCHOOL_TYPE_MAP[s].get("type") == school_type]
|
||
|
||
thermometers = {}
|
||
for sd_name, sd_data in report_data["sub_dimensions"].items():
|
||
# 同类学校均值
|
||
if same_type_schools and sd_name in all_sub_scores:
|
||
st_vals = [float(all_sub_scores[sd_name].get(s, 50)) for s in same_type_schools]
|
||
same_type_avg = round(sum(st_vals) / len(st_vals), 2)
|
||
else:
|
||
same_type_avg = round(sd_data["district_avg"], 2)
|
||
|
||
# 水平阈值
|
||
from ..config import LEVEL_THRESHOLDS
|
||
thresholds = LEVEL_THRESHOLDS.get(sd_name, {})
|
||
|
||
thermometers[sd_name] = {
|
||
"score": round(sd_data["score"], 2),
|
||
"district_avg": round(sd_data["district_avg"], 2),
|
||
"same_type_avg": same_type_avg,
|
||
"level": sd_data.get("level", 0),
|
||
"thresholds": {
|
||
"level4": thresholds.get("level4", 57),
|
||
"level3": thresholds.get("level3", 50),
|
||
"level2": thresholds.get("level2", 43),
|
||
},
|
||
}
|
||
|
||
return {
|
||
"school_name": school,
|
||
"data": thermometers,
|
||
}
|
||
|
||
def _build_waterfall_chart(self, report_data: Dict) -> Dict:
|
||
"""
|
||
D. 进步空间瀑布图
|
||
展示:如果每个低于水平3的子维度提升到水平3的阈值,总分增加多少
|
||
让校长看到"改哪几个点收益最大"
|
||
"""
|
||
from ..config import LEVEL_THRESHOLDS
|
||
|
||
school = report_data["school"]
|
||
current_total = report_data["overall"]["score"]
|
||
|
||
# 找出所有低于水平3的子维度
|
||
improvement_items = []
|
||
for sd_name, sd_data in report_data["sub_dimensions"].items():
|
||
level = sd_data.get("level", 0)
|
||
if level < 3:
|
||
current_score = sd_data["score"]
|
||
# 提升到水平3的阈值
|
||
target_score = LEVEL_THRESHOLDS.get(sd_name, {}).get("level3", 50)
|
||
gap = round(target_score - current_score, 2)
|
||
if gap > 0:
|
||
improvement_items.append({
|
||
"name": sd_name,
|
||
"parent": sd_data.get("parent_dimension", ""),
|
||
"current_score": round(current_score, 2),
|
||
"target_score": round(target_score, 2),
|
||
"gap": gap,
|
||
"current_level": level,
|
||
})
|
||
|
||
# 按收益从大到小排序
|
||
improvement_items.sort(key=lambda x: x["gap"], reverse=True)
|
||
|
||
# 估算总分提升(简化:假设20个子维度等权重影响总分)
|
||
total_sub_dims = len(report_data["sub_dimensions"])
|
||
cumulative = current_total
|
||
waterfall_steps = [{"name": "当前总分", "value": round(current_total, 2), "type": "current"}]
|
||
|
||
for item in improvement_items:
|
||
# 粗略估算:子维度提升gap分 → 总分提升 gap / total_sub_dims * 权重
|
||
# 实际PCA权重不同,此处用等权近似
|
||
estimated_gain = round(item["gap"] / total_sub_dims, 2)
|
||
cumulative += estimated_gain
|
||
waterfall_steps.append({
|
||
"name": item["name"],
|
||
"value": round(estimated_gain, 2),
|
||
"type": "gain",
|
||
"detail": f"从水平{item['current_level']}→水平3 (+{item['gap']}分)",
|
||
})
|
||
|
||
waterfall_steps.append({"name": "潜在总分", "value": round(cumulative, 2), "type": "potential"})
|
||
|
||
return {
|
||
"school_name": school,
|
||
"current_total": round(current_total, 2),
|
||
"potential_total": round(cumulative, 2),
|
||
"total_gain": round(cumulative - current_total, 2),
|
||
"steps": waterfall_steps,
|
||
"improvements": improvement_items,
|
||
}
|
||
|
||
@staticmethod
|
||
def _to_namespace(d: Dict) -> Dict:
|
||
"""将dict转为可用点号访问的对象(Jinja2兼容)"""
|
||
class Namespace(dict):
|
||
def __getattr__(self, key):
|
||
try:
|
||
return self[key]
|
||
except KeyError:
|
||
return None
|
||
return Namespace(d) if isinstance(d, dict) else d
|