Initial commit

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
lofyer
2026-07-13 15:38:41 +08:00
co-authored by factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
commit 71db82393a
180 changed files with 170640 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
+255
View File
@@ -0,0 +1,255 @@
"""
数据引擎:Excel解析 + 数据清洗 + 赋分计算
负责将原始Excel数据转换为每所学校的结构化得分数据
"""
import pandas as pd
import numpy as np
from pathlib import Path
from typing import Dict, List, Optional, Tuple
import logging
from ..config import (
EXCEL_BASIC_INFO, EXCEL_COURSE_IMPL, EXCEL_SUBJECT_IMPL,
SCHOOL_NAME_MAP, SUBJECTS, SCHOOL_TYPE_MAP,
)
logger = logging.getLogger(__name__)
class DataEngine:
"""数据引擎:读取Excel、清洗、结构化"""
def __init__(self, data_dir: Optional[Path] = None):
self.data_dir = data_dir
self._basic_info: Optional[pd.DataFrame] = None
self._course_impl: Optional[pd.DataFrame] = None
self._subject_impl: Optional[pd.DataFrame] = None
def load_all(self) -> None:
"""加载所有Excel数据"""
logger.info("开始加载Excel数据...")
self._basic_info = self._load_basic_info()
self._course_impl = self._load_course_impl()
self._subject_impl = self._load_subject_impl()
logger.info(
f"数据加载完成: 基础信息={len(self._basic_info)}行, "
f"课程实施={len(self._course_impl)}行, "
f"学科课程={len(self._subject_impl)}"
)
def _standardize_school_name(self, df: pd.DataFrame, col: str) -> pd.DataFrame:
"""标准化学校名称"""
df[col] = df[col].map(lambda x: SCHOOL_NAME_MAP.get(x, x))
return df
def _load_basic_info(self) -> pd.DataFrame:
"""加载基础信息表"""
df = pd.read_excel(EXCEL_BASIC_INFO)
df = self._standardize_school_name(df, "学校名称")
return df
def _load_course_impl(self) -> pd.DataFrame:
"""加载课程实施情况表"""
df = pd.read_excel(EXCEL_COURSE_IMPL)
df = self._standardize_school_name(df, "学校简称")
# 统一列名
df = df.rename(columns={"学校简称": "学校名称"})
return df
def _load_subject_impl(self) -> pd.DataFrame:
"""加载学科课程实施情况表"""
df = pd.read_excel(EXCEL_SUBJECT_IMPL)
df = self._standardize_school_name(df, "学校简称")
df = df.rename(columns={"学校简称": "学校名称"})
return df
@property
def schools(self) -> List[str]:
"""获取所有学校名称列表"""
if self._course_impl is None:
self.load_all()
return sorted(self._course_impl["学校名称"].unique().tolist())
def get_school_basic_info(self, school: str) -> Dict:
"""获取学校基本信息"""
if self._basic_info is None:
self.load_all()
df = self._basic_info[self._basic_info["学校名称"] == school]
info = SCHOOL_TYPE_MAP.get(school, {})
# 提取关键字段
def _get_field(field_name):
rows = df[df["字段名称"] == field_name]
if len(rows) > 0:
return rows.iloc[0]["字段值"]
return None
info.update({
"school_name": school,
"建校年份": _get_field("建校年份"),
"教师总数": _get_field("学校教师总数"),
"占地面积": _get_field("占地面积"),
"建筑面积": _get_field("建筑面积"),
})
return info
def get_school_course_data(self, school: str) -> pd.DataFrame:
"""获取某学校的课程实施情况数据"""
if self._course_impl is None:
self.load_all()
return self._course_impl[self._course_impl["学校名称"] == school].copy()
def get_school_subject_data(self, school: str, subject: Optional[str] = None) -> pd.DataFrame:
"""获取某学校的学科课程实施数据"""
if self._subject_impl is None:
self.load_all()
df = self._subject_impl[self._subject_impl["学校名称"] == school].copy()
if subject:
df = df[df["学科"] == subject]
return df
# ========== 课程领导力相关数据提取 ==========
def get_weekly_hours(self, school: str) -> pd.DataFrame:
"""获取学校各学科各年级各学期的周课时数据"""
df = self.get_school_course_data(school)
# 筛选周课时相关字段
hours_fields = ["学科必修课周课时", "学科选择性必修课周课时", "学科类选修课周课时"]
result = df[df["字段名称"].isin(hours_fields)].copy()
result["字段取值"] = pd.to_numeric(result["字段取值"], errors="coerce")
return result
def get_course_norms(self, school: str) -> Dict:
"""获取学校课程规范落实相关数据(建设规范、档案等)"""
df = self.get_school_course_data(school)
norm_keywords = ["建设规范文本", "档案", "已经建成并使用", "尚未建成"]
mask = df["字段名称"].apply(
lambda x: any(k in str(x) for k in norm_keywords) if pd.notna(x) else False
)
return df[mask][["字段名称", "字段取值"]].to_dict("records")
# ========== 教学变革力相关数据提取 ==========
def get_teaching_reform_data(self, school: str) -> Dict:
"""获取教学方式变革相关数据(认识程度、落实程度、实施方式)"""
df = self.get_school_subject_data(school)
reform_keywords = [
"认识程度", "落实程度", "认识", "落实",
"理解式学习", "自主性学习", "实践性学习", "跨学科学习",
"信息技术与教学融合", "信息融入教学",
]
mask = df["字段名称"].apply(
lambda x: any(k in str(x) for k in reform_keywords) if pd.notna(x) else False
)
return df[mask]
def get_homework_data(self, school: str) -> Dict:
"""获取作业设计与管理数据"""
df = self.get_school_subject_data(school)
hw_keywords = [
"作业", "实践类", "表现类", "跨学科", "团队合作",
"批改", "评价", "属性标注", "时长控制",
]
mask = df["字段名称"].apply(
lambda x: any(k in str(x) for k in hw_keywords) if pd.notna(x) else False
)
return df[mask]
# ========== 通用数据提取方法 ==========
def get_field_value(self, school: str, source: str, field_name: str,
subject: Optional[str] = None) -> Optional[str]:
"""通用字段值获取"""
if source == "basic":
df = self._basic_info[self._basic_info["学校名称"] == school]
col = "字段名称"
val_col = "字段值"
elif source == "course":
df = self.get_school_course_data(school)
col = "字段名称"
val_col = "字段取值"
elif source == "subject":
df = self.get_school_subject_data(school, subject)
col = "字段名称"
val_col = "字段取值"
else:
return None
rows = df[df[col] == field_name]
if len(rows) > 0:
return rows.iloc[0][val_col]
return None
def get_field_by_id(self, school: str, source: str, field_id: str,
subject: Optional[str] = None) -> List[Dict]:
"""通过字段ID获取数据"""
if source == "basic":
df = self._basic_info[self._basic_info["学校名称"] == school]
elif source == "course":
df = self.get_school_course_data(school)
elif source == "subject":
df = self.get_school_subject_data(school, subject)
else:
return []
rows = df[df["字段ID"] == field_id]
return rows.to_dict("records")
def build_score_matrix(self) -> pd.DataFrame:
"""
构建所有学校×所有字段的得分矩阵
这是赋分引擎的输入
"""
if self._course_impl is None:
self.load_all()
records = []
for school in self.schools:
# 课程实施表数据
course_df = self.get_school_course_data(school)
for _, row in course_df.iterrows():
records.append({
"学校": school,
"来源": "course",
"题号": row.get("题号"),
"字段ID": row.get("字段ID"),
"字段名称": row.get("字段名称"),
"字段取值": row.get("字段取值"),
"学科": row.get("学科", "不分学科"),
"年级": row.get("年级", "不分年级"),
"学期": row.get("学期", ""),
})
# 学科课程表数据
subject_df = self.get_school_subject_data(school)
for _, row in subject_df.iterrows():
records.append({
"学校": school,
"来源": "subject",
"题号": row.get("题号"),
"字段ID": row.get("字段ID"),
"字段名称": row.get("字段名称"),
"字段取值": row.get("字段取值"),
"学科": row.get("学科", "不分学科"),
"年级": "",
"学期": "",
})
matrix = pd.DataFrame(records)
logger.info(f"得分矩阵构建完成: {len(matrix)}行, {len(self.schools)}所学校")
return matrix
def summary(self) -> Dict:
"""数据摘要"""
if self._basic_info is None:
self.load_all()
return {
"schools": self.schools,
"school_count": len(self.schools),
"basic_info_rows": len(self._basic_info),
"course_impl_rows": len(self._course_impl),
"subject_impl_rows": len(self._subject_impl),
"subjects": SUBJECTS,
"school_types": {s: SCHOOL_TYPE_MAP[s]["type"] for s in self.schools},
}
File diff suppressed because it is too large Load Diff
+777
View File
@@ -0,0 +1,777 @@
"""
报告渲染引擎:将数据+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
+850
View File
@@ -0,0 +1,850 @@
"""
赋分引擎:将原始题目回答按赋分规则转换为分数
基于赋分整理表的规则,实现各类赋分函数
"""
import pandas as pd
import numpy as np
from typing import Dict, List, Optional, Tuple
import logging
import re
from ..config import DIMENSION_FRAMEWORK, SUBJECTS
from .data_engine import DataEngine
logger = logging.getLogger(__name__)
class ScoringEngine:
"""
赋分引擎
赋分逻辑基于"高中课程实施监测指标、题目、赋分整理0127.xlsx"
核心策略:按维度分组,从原始数据中提取相关字段,按规则赋分,
然后将赋分结果交给统计引擎做PCA合成
"""
def __init__(self, data_engine: DataEngine):
self.data_engine = data_engine
def score_all_schools(self) -> Dict[str, Dict[str, List[float]]]:
"""
对所有学校的所有维度进行赋分
返回: {
school_name: {
sub_dimension_name: [score1, score2, ...]
}
}
"""
result = {}
for school in self.data_engine.schools:
logger.info(f"正在对 {school} 进行赋分...")
result[school] = self._score_school(school)
return result
def _score_school(self, school: str) -> Dict[str, List[float]]:
"""对单个学校进行全维度赋分"""
scores = {}
# 课程领导力
scores["国家标准遵循"] = self._score_national_standard(school)
scores["课程结构建设"] = self._score_course_structure(school)
scores["课程规范落实"] = self._score_course_norms(school)
# 教学变革力
scores["教学方式变革"] = self._score_teaching_reform(school)
scores["作业设计与管理变革"] = self._score_homework_reform(school)
# 学生发展指导力
scores["学科发展的个性化辅导"] = self._score_personalized_tutoring(school)
scores["学生生涯发展指导"] = self._score_career_guidance(school)
# 教师发展支持力
scores["培训支持"] = self._score_training_support(school)
scores["教研支持"] = self._score_research_support(school)
scores["项目支持"] = self._score_project_support(school)
# 教育质量评估力
scores["科学评价观"] = self._score_scientific_evaluation(school)
scores["学业质量评估"] = self._score_academic_evaluation(school)
scores["综合素质评估"] = self._score_comprehensive_evaluation(school)
scores["实践活动评估"] = self._score_practice_evaluation(school)
# 教育条件保障力
scores["区域推进"] = self._score_regional_promotion(school)
scores["环境支持"] = self._score_environment_support(school)
scores["资源支持"] = self._score_resource_support(school)
# 数字化赋能力
scores["教学方式创新"] = self._score_digital_teaching(school)
scores["评价精准化与个性化"] = self._score_digital_evaluation(school)
scores["课程迭代优化"] = self._score_digital_curriculum(school)
return scores
# ========== 课程领导力 ==========
def _score_national_standard(self, school: str) -> List[float]:
"""
国家标准遵循:开足开齐国家课程
赋分:必修课程学分低于标准=0,高于标准=1,与标准一致=2
选必/选修课程:低于标准=0,达到标准=1
"""
scores = []
hours_df = self.data_engine.get_weekly_hours(school)
if len(hours_df) == 0:
return [1.0] # 默认中等
# 按课程类型汇总
for course_type, field_name in [
("必修", "学科必修课周课时"),
("选必", "学科选择性必修课周课时"),
("选修", "学科类选修课周课时"),
]:
type_df = hours_df[hours_df["字段名称"] == field_name]
if len(type_df) == 0:
scores.append(0.5)
continue
# 按学科汇总总课时
total = type_df.groupby("学科")["字段取值"].sum()
has_courses = (total > 0).sum()
total_hours = total.sum()
if course_type == "必修":
# 必修课:与标准一致=2,高于=1,低于=0
# 简化处理:有多少学科开了课
exam_subjects = ["语文", "数学", "英语", "物理", "化学", "生物学",
"历史", "地理", "思想政治"]
opened = sum(1 for s in exam_subjects if s in total.index and total.get(s, 0) > 0)
if opened >= len(exam_subjects):
scores.append(2.0)
elif opened >= 6:
scores.append(1.0)
else:
scores.append(0.0)
else:
# 选必/选修:达到标准=1,低于=0
if total_hours > 0:
scores.append(1.0)
else:
scores.append(0.0)
return scores if scores else [1.0]
def _score_course_structure(self, school: str) -> List[float]:
"""
课程结构建设:学科类课程结构、校本特色课程结构、综合实践活动与劳动
赋分:按照离差百分比和填报数值
"""
scores = []
hours_df = self.data_engine.get_weekly_hours(school)
if len(hours_df) > 0:
# 1. 学科类课程结构:各学科三类课程的离差
by_subject = hours_df.groupby(["学科", "字段名称"])["字段取值"].sum().unstack(fill_value=0)
if len(by_subject) > 0:
total_per_subject = by_subject.sum(axis=1)
overall_total = total_per_subject.sum()
if overall_total > 0:
proportions = total_per_subject / overall_total
mean_prop = proportions.mean()
deviation = np.abs(proportions - mean_prop).sum()
# 离差越小越好,标准化到0-3分
structure_score = max(0, 3 - deviation * 10)
scores.append(structure_score)
else:
scores.append(1.0)
else:
scores.append(1.0)
# 2. 校本特色课程:跨学科选修课门数、综合主题选修课门数
course_df = self.data_engine.get_school_course_data(school)
for field in ["跨学科选修课门数", "综合主题选修课门数", "综合实践选修课门数"]:
vals = course_df[course_df["字段名称"] == field]["字段取值"]
if len(vals) > 0:
try:
v = float(vals.iloc[0])
scores.append(min(v / 5, 3.0)) # 归一化
except (ValueError, TypeError):
scores.append(0.5)
# 3. 综合实践活动与劳动
for field in ["三年应完成的研究性学习数量", "三年社会考察个数", "三年志愿服务时长"]:
vals = course_df[course_df["字段名称"] == field]["字段取值"]
if len(vals) > 0:
try:
v = float(vals.iloc[0])
scores.append(min(v / 10, 3.0))
except (ValueError, TypeError):
scores.append(0.5)
return scores if scores else [1.0]
def _score_course_norms(self, school: str) -> List[float]:
"""
课程规范落实:建设规范文本 + 档案规范
赋分:有=2,无=0
"""
scores = []
norms = self.data_engine.get_course_norms(school)
norm_score = 0
archive_score = 0
norm_count = 0
archive_count = 0
for n in norms:
name = str(n.get("字段名称", ""))
val = str(n.get("字段取值", ""))
if "建设规范文本" in name:
norm_count += 1
if val and val not in ["0", "nan", "None", ""]:
norm_score += 2
elif "档案" in name:
archive_count += 1
if "已经建成并使用" in name and val == "1":
archive_score += 2
elif "已经建成" in name and val == "1":
archive_score += 1
if norm_count > 0:
scores.append(norm_score / norm_count * 2)
if archive_count > 0:
scores.append(archive_score / archive_count * 2)
return scores if scores else [1.0]
# ========== 教学变革力 ==========
def _score_teaching_reform(self, school: str) -> List[float]:
"""
教学方式变革:认识程度 + 落实程度 + 实施方式
赋分:量表题4/3/2/1分;多选每项1分加总
"""
scores = []
subject_df = self.data_engine.get_school_subject_data(school)
for subject in SUBJECTS:
sub_df = subject_df[subject_df["学科"] == subject]
sub_scores = []
# 认识程度类题目
for keyword in ["认识程度", "认识"]:
rows = sub_df[sub_df["字段名称"].str.contains(keyword, na=False)]
for _, row in rows.iterrows():
s = self._score_likert(row["字段取值"], reverse=False)
if s is not None:
sub_scores.append(s)
# 落实程度类题目
for keyword in ["落实程度", "落实"]:
rows = sub_df[sub_df["字段名称"].str.contains(keyword, na=False)]
for _, row in rows.iterrows():
s = self._score_likert(row["字段取值"], reverse=False)
if s is not None:
sub_scores.append(s)
# 实施方式(多选,体现形式类)
form_rows = sub_df[sub_df["字段名称"].str.contains("体现形式", na=False)]
if len(form_rows) > 0:
form_count = (form_rows["字段取值"].astype(str) == "1").sum()
sub_scores.append(min(form_count, 6))
if sub_scores:
scores.append(np.mean(sub_scores))
return scores if scores else [2.0]
def _score_homework_reform(self, school: str) -> List[float]:
"""
作业设计与管理变革:作业设计 + 作业管理
"""
scores = []
subject_df = self.data_engine.get_school_subject_data(school)
for subject in SUBJECTS:
sub_df = subject_df[subject_df["学科"] == subject]
sub_scores = []
# 作业设计:实践类/表现类/跨学科/团队合作作业是否布置
for hw_type in ["实践类作业_有布置", "表现类作业_有布置", "跨学科作业_有布置", "团队合作类作业_有布置"]:
rows = sub_df[sub_df["字段名称"] == hw_type]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
sub_scores.append(1.0)
else:
sub_scores.append(0.0)
# 作业属性标注(多选)
attr_rows = sub_df[sub_df["字段名称"].str.contains("作业属性标注", na=False)]
if len(attr_rows) > 0:
attr_count = (attr_rows["字段取值"].astype(str) == "1").sum()
sub_scores.append(min(attr_count, 5))
# 作业管理:时长控制、批改、评价
for field, score_map in [
("回家作业时长控制_学校控制", 3),
("回家作业时长控制_教研组负责", 2),
("作业批改范围_全部批改", 3),
("作业批改范围_部分练习", 1),
]:
rows = sub_df[sub_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
sub_scores.append(score_map)
if sub_scores:
scores.append(np.mean(sub_scores))
return scores if scores else [1.5]
# ========== 学生发展指导力 ==========
def _score_personalized_tutoring(self, school: str) -> List[float]:
"""个性化辅导"""
scores = []
subject_df = self.data_engine.get_school_subject_data(school)
for subject in SUBJECTS:
sub_df = subject_df[subject_df["学科"] == subject]
# 辅导时长
for field, val in [
("个别辅导时长_每周>2h", 4),
("个别辅导时长_每周1~2h", 3),
("个别辅导时长_每周<1h", 2),
("个别辅导时长_几乎无", 1),
]:
rows = sub_df[sub_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(val)
break
# 辅导方式
for field, val in [
("个别辅导实施方式_分散辅导", 3),
("个别辅导实施方式_分组统一辅导", 2),
("个别辅导实施方式_班级统一辅导", 1),
]:
rows = sub_df[sub_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(val)
break
return scores if scores else [2.0]
def _score_career_guidance(self, school: str) -> List[float]:
"""生涯发展指导"""
scores = []
course_df = self.data_engine.get_school_course_data(school)
# 生涯指导实施方式
for field, val in [
("生涯指导实施方式_专设课程", 3),
("生涯指导实施方式_社会考察和志愿服务", 2),
]:
rows = course_df[course_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(val)
# 覆盖率
rows = course_df[course_df["字段名称"] == "完成生涯指导的学生占比_90%+"]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(4)
else:
scores.append(2)
# 教师构成
for field, val in [
("生涯指导教师构成_本校和外聘结合", 4),
("生涯指导教师构成_本校为主", 3),
]:
rows = course_df[course_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(val)
break
# 资源支持
for resource in ["生涯指导的校外资源支持程度", "生涯指导的校内资源支持程度"]:
rows = course_df[course_df["字段名称"] == resource]
if len(rows) > 0:
s = self._score_resource_level(rows.iloc[0]["字段取值"])
if s is not None:
scores.append(s)
return scores if scores else [2.0]
# ========== 教师发展支持力 ==========
def _score_training_support(self, school: str) -> List[float]:
"""培训支持"""
scores = []
subject_df = self.data_engine.get_school_subject_data(school)
for subject in SUBJECTS:
sub_df = subject_df[subject_df["学科"] == subject]
# 提供外校培训的教师人数
rows = sub_df[sub_df["字段名称"] == "提供外校培训的教师人数"]
if len(rows) > 0:
try:
v = float(rows.iloc[0]["字段取值"])
scores.append(min(v, 5))
except (ValueError, TypeError):
pass
# 区域培训指导人数
rows = sub_df[sub_df["字段名称"] == "区域培训指导人数"]
if len(rows) > 0:
try:
v = float(rows.iloc[0]["字段取值"])
scores.append(min(v, 5))
except (ValueError, TypeError):
pass
return scores if scores else [1.0]
def _score_research_support(self, school: str) -> List[float]:
"""教研支持"""
scores = []
subject_df = self.data_engine.get_school_subject_data(school)
for subject in SUBJECTS:
sub_df = subject_df[subject_df["学科"] == subject]
# 教研活动次数
rows = sub_df[sub_df["字段名称"] == "学科教研组每学期活动次数"]
if len(rows) > 0:
try:
v = float(rows.iloc[0]["字段取值"])
scores.append(min(v / 3, 4)) # 归一化
except (ValueError, TypeError):
pass
# 教研活动时长
rows = sub_df[sub_df["字段名称"] == "学科教研组平均每次活动时长"]
if len(rows) > 0:
try:
v = float(rows.iloc[0]["字段取值"])
scores.append(min(v / 30, 4)) # 30分钟为基准
except (ValueError, TypeError):
pass
# 教研计划
rows = sub_df[sub_df["字段名称"] == "学科教研工作计划_有"]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(2)
# 校级展示
for field in ["学科教研组校级展示_有", "学科教研组区域展示_有", "学科教研组成果发表_有"]:
rows = sub_df[sub_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(2)
return scores if scores else [1.0]
def _score_project_support(self, school: str) -> List[float]:
"""项目支持"""
scores = []
subject_df = self.data_engine.get_school_subject_data(school)
course_df = self.data_engine.get_school_course_data(school)
# 学科层面的项目
for subject in SUBJECTS:
sub_df = subject_df[subject_df["学科"] == subject]
for field in ["学科承担的市级教改项目个数", "学科承担的区级教改项目个数", "学科承担的校级教改项目个数"]:
rows = sub_df[sub_df["字段名称"] == field]
if len(rows) > 0:
try:
v = float(rows.iloc[0]["字段取值"])
scores.append(min(v, 5))
except (ValueError, TypeError):
pass
# 学校层面的项目
for field in ["学校负责的市级教改项目个数", "学校参与的市级教改项目个数",
"学校负责的区级教改项目个数", "学校参与的区级教改项目个数",
"校级教改项目个数"]:
rows = course_df[course_df["字段名称"] == field]
if len(rows) > 0:
try:
v = float(rows.iloc[0]["字段取值"])
scores.append(min(v, 5))
except (ValueError, TypeError):
pass
return scores if scores else [0.5]
# ========== 教育质量评估力 ==========
def _score_scientific_evaluation(self, school: str) -> List[float]:
"""科学评价观"""
scores = []
subject_df = self.data_engine.get_school_subject_data(school)
for subject in SUBJECTS:
sub_df = subject_df[subject_df["学科"] == subject]
# 作业评价关注点(多选,关注素养发展的项目越多越好)
eval_items = sub_df[sub_df["字段名称"].str.contains("作业评价关注点|课堂表现评价关注点|学科实践活动评价关注点", na=False)]
if len(eval_items) > 0:
focus_count = (eval_items["字段取值"].astype(str) == "1").sum()
scores.append(min(focus_count / 3, 4))
return scores if scores else [2.0]
def _score_academic_evaluation(self, school: str) -> List[float]:
"""学业质量评估"""
scores = []
subject_df = self.data_engine.get_school_subject_data(school)
for subject in SUBJECTS:
sub_df = subject_df[subject_df["学科"] == subject]
# 评价工具建成
for field, val in [
("作业评价工具_已经建成并使用", 2),
("课堂表现评价工具_已经建成并使用", 2),
("作业评价工具_已经建成尚未使用", 1),
("作业评价工具_尚未建成和使用", 0),
]:
rows = sub_df[sub_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(val)
break
# 考试分析
for field, val in [
("学期考试分析_执行分析并存档", 3),
("学期考试分析_执行分析,不要求存档", 2),
("学期考试分析_教师自己决定", 1),
]:
rows = sub_df[sub_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(val)
break
# 表现性评价应用
for field, val in [
("表现性评价应用程度_经常", 4),
("表现性评价应用程度_有时", 3),
("表现性评价应用程度_总是", 4),
("表现性评价应用程度_从不", 1),
]:
rows = sub_df[sub_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(val)
break
return scores if scores else [2.0]
def _score_comprehensive_evaluation(self, school: str) -> List[float]:
"""综合素质评估"""
scores = []
course_df = self.data_engine.get_school_course_data(school)
# 综评评价体系
for field, val in [
("综评评价体系_已经建成并使用", 3),
("综评评价体系_已经建成尚未使用", 2),
("综评评价体系_未建成", 1),
("综评评价体系_不准备建设", 0),
]:
rows = course_df[course_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(val)
break
# 综评信息化
for field, val in [
("综评信息化实现_自建平台支持", 2),
("综评信息化实现_借助第三方平台支持", 1),
]:
rows = course_df[course_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(val)
break
# 综评结果使用(多选)
result_uses = course_df[course_df["字段名称"].str.contains("综评结果使用|综评应用", na=False)]
if len(result_uses) > 0:
use_count = (result_uses["字段取值"].astype(str) == "1").sum()
scores.append(min(use_count, 6))
return scores if scores else [1.0]
def _score_practice_evaluation(self, school: str) -> List[float]:
"""实践活动评估"""
scores = []
course_df = self.data_engine.get_school_course_data(school)
# 研究性学习评价
for field, val in [
("研究性学习评价工具_已经建成并使用", 2),
("研究性学习评价工具_尚未建成和使用", 0),
]:
rows = course_df[course_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(val)
break
# 社会考察评价
for field, val in [
("社会考察评价工具_已经建成并使用", 2),
("社会考察评价工具_尚未建成和使用", 0),
]:
rows = course_df[course_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(val)
break
# 学科实践活动评价
subject_df = self.data_engine.get_school_subject_data(school)
for subject in SUBJECTS[:5]: # 抽样几个学科
sub_df = subject_df[subject_df["学科"] == subject]
for field, val in [
("学科实践活动工具_已经建成并使用", 2),
("学科实践活动工具_已经建成尚未使用", 1),
("学科实践活动工具_尚未建成和使用", 0),
]:
rows = sub_df[sub_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(val)
break
return scores if scores else [1.0]
# ========== 教育条件保障力 ==========
def _score_regional_promotion(self, school: str) -> List[float]:
"""区域推进"""
scores = []
course_df = self.data_engine.get_school_course_data(school)
# 工作会频率
for field, val in [
("区域工作会参与_一月4次以上", 7),
("区域工作会参与_一月1次", 5),
("区域工作会参与_二月1次", 3),
("区域工作会参与_三月1次", 1),
]:
rows = course_df[course_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(val)
break
# 区域推进措施(多选)
measures = course_df[course_df["字段名称"].str.contains("区域推进措施", na=False)]
if len(measures) > 0:
measure_count = (measures["字段取值"].astype(str) == "1").sum()
scores.append(min(measure_count, 5))
return scores if scores else [2.0]
def _score_environment_support(self, school: str) -> List[float]:
"""环境支持:硬件+信息化"""
scores = []
course_df = self.data_engine.get_school_course_data(school)
# 场馆供给
for field, val in [
("场馆供给_能满足需要", 3),
("场馆供给_基本满足需要", 2),
("场馆供给_难以满足需要", 1),
]:
rows = course_df[course_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(val)
break
# 专用教室供给
for field, val in [
("专用教室供给_能满足需要", 3),
("专用教室供给_基本满足需要", 2),
]:
rows = course_df[course_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(val)
break
# 信息化平台功能(多选)
info_funcs = course_df[course_df["字段名称"].str.contains("信息化平台功能", na=False)]
if len(info_funcs) > 0:
func_count = (info_funcs["字段取值"].astype(str) == "1").sum()
scores.append(min(func_count / 3, 5))
return scores if scores else [2.0]
def _score_resource_support(self, school: str) -> List[float]:
"""资源支持:校内资源 + 校外资源 + 师资配置"""
scores = []
subject_df = self.data_engine.get_school_subject_data(school)
for subject in SUBJECTS:
sub_df = subject_df[subject_df["学科"] == subject]
# 校内资源支持程度
for field in ["必修课校内资源支持程度", "选择性必修课校内资源支持程度"]:
rows = sub_df[sub_df["字段名称"] == field]
if len(rows) > 0:
s = self._score_resource_level(rows.iloc[0]["字段取值"])
if s is not None:
scores.append(s)
# 校外资源支持程度
for field in ["必修课校外资源支持程度", "选择性必修课校外资源支持程度"]:
rows = sub_df[sub_df["字段名称"] == field]
if len(rows) > 0:
s = self._score_resource_level(rows.iloc[0]["字段取值"])
if s is not None:
scores.append(s)
# 师资:教研组总人数、高级教师比例等
rows = sub_df[sub_df["字段名称"] == "学科教研组总人数"]
if len(rows) > 0:
try:
total = float(rows.iloc[0]["字段取值"])
scores.append(min(total / 3, 4))
except (ValueError, TypeError):
pass
return scores if scores else [2.0]
# ========== 数字化赋能力 ==========
def _score_digital_teaching(self, school: str) -> List[float]:
"""教学方式创新"""
scores = []
subject_df = self.data_engine.get_school_subject_data(school)
for subject in SUBJECTS:
sub_df = subject_df[subject_df["学科"] == subject]
# 信息技术融合认识
for field in ["信息技术与教学融合的认识_所有人可做到"]:
rows = sub_df[sub_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(4)
break
else:
rows = sub_df[sub_df["字段名称"] == "信息技术与教学融合的认识_个别人可做到"]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(1)
# 信息化终端使用比例
for field, val in [
("信息化终端使用比例_80%+", 4),
("信息化终端使用比例_60~79%", 3),
("信息化终端使用比例_30~59%", 2),
("信息化终端使用比例_30%-", 1),
]:
rows = sub_df[sub_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(val)
break
return scores if scores else [2.0]
def _score_digital_evaluation(self, school: str) -> List[float]:
"""评价精准化与个性化"""
scores = []
subject_df = self.data_engine.get_school_subject_data(school)
course_df = self.data_engine.get_school_course_data(school)
# 学业评价信息化
for subject in SUBJECTS:
sub_df = subject_df[subject_df["学科"] == subject]
for field, val in [
("学业评价信息化实现_自建平台支持", 3),
("学业评价信息化实现_借助第三方平台支持", 2),
("学业评价信息化实现_没有平台支持", 0),
]:
rows = sub_df[sub_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(val)
break
# 学校层面的信息化支持
for field, val in [
("学校信息系统对选课支持程度", None),
("学校信息系统对排课支持程度", None),
]:
rows = course_df[course_df["字段名称"] == field]
if len(rows) > 0:
s = self._score_resource_level(rows.iloc[0]["字段取值"])
if s is not None:
scores.append(s)
return scores if scores else [1.5]
def _score_digital_curriculum(self, school: str) -> List[float]:
"""课程迭代优化"""
scores = []
course_df = self.data_engine.get_school_course_data(school)
# 数据连通
for field, val in [
("数据连通_有数据能互通", 3),
("数据连通_有数据不互通", 1),
]:
rows = course_df[course_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(val)
break
# 管理业务信息化
for field, val in [
("管理业务的信息化应用_绝大部分", 4),
("教学业务的信息化应用_绝大部分", 4),
]:
rows = course_df[course_df["字段名称"] == field]
if len(rows) > 0 and str(rows.iloc[0]["字段取值"]) == "1":
scores.append(val)
# 已完成网络课程数
rows = course_df[course_df["字段名称"] == "已完成的网络课程门数"]
if len(rows) > 0:
try:
v = float(rows.iloc[0]["字段取值"])
scores.append(min(v / 3, 4))
except (ValueError, TypeError):
pass
return scores if scores else [1.5]
# ========== 辅助赋分函数 ==========
@staticmethod
def _score_likert(value, scale: int = 4, reverse: bool = False) -> Optional[float]:
"""
量表题赋分
value格式可能是 "1""2(有一些支持)"
"""
try:
val_str = str(value).strip()
# 提取数字部分
match = re.match(r'^(\d+)', val_str)
if match:
v = int(match.group(1))
if reverse:
return float(scale + 1 - v)
return float(v)
except (ValueError, TypeError):
pass
return None
@staticmethod
def _score_resource_level(value) -> Optional[float]:
"""资源支持程度赋分:几乎没有=1, 有一些=2, 有足够=3"""
val_str = str(value).strip()
match = re.match(r'^(\d+)', val_str)
if match:
v = int(match.group(1))
return float(v)
if "足够" in val_str or "3" in val_str:
return 3.0
elif "一些" in val_str or "2" in val_str:
return 2.0
elif "没有" in val_str or "1" in val_str:
return 1.0
return None
+363
View File
@@ -0,0 +1,363 @@
"""
统计引擎:PCA合成 + 标准化 + 水平判定 + 聚类 + T检验 + 相关性
将赋分后的原始数据合成为维度得分,并进行统计分析
"""
import pandas as pd
import numpy as np
from typing import Dict, List, Optional, Tuple
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
from scipy import stats
import logging
from ..config import (
PCA_MEAN, PCA_STD, LEVEL_THRESHOLDS, LEVEL_DESCRIPTIONS,
DIMENSION_FRAMEWORK, SUBJECTS, SCHOOL_TYPE_MAP,
)
logger = logging.getLogger(__name__)
class StatsEngine:
"""统计引擎"""
def __init__(self):
self._dimension_scores: Optional[pd.DataFrame] = None
self._sub_dimension_scores: Optional[pd.DataFrame] = None
self._subject_dimension_scores: Optional[pd.DataFrame] = None
def standardize_scores(self, raw_scores: np.ndarray) -> np.ndarray:
"""标准化到均值50标准差10"""
if len(raw_scores) < 2:
return np.full_like(raw_scores, PCA_MEAN, dtype=float)
mean = np.nanmean(raw_scores)
std = np.nanstd(raw_scores, ddof=1)
if std == 0 or np.isnan(std):
return np.full_like(raw_scores, PCA_MEAN, dtype=float)
return (raw_scores - mean) / std * PCA_STD + PCA_MEAN
def pca_compose(self, data_matrix: pd.DataFrame) -> np.ndarray:
"""
PCA合成:将多个变量合成为一个主成分分数
data_matrix: 行=学校, 列=变量
返回:合成后的分数(已标准化到50/10)
"""
# 处理缺失值:均值填充
filled = data_matrix.fillna(data_matrix.mean())
if filled.shape[1] == 0:
return np.full(filled.shape[0], PCA_MEAN)
if filled.shape[1] == 1:
# 只有一个变量,直接标准化
return self.standardize_scores(filled.iloc[:, 0].values)
# 标准化
scaler = StandardScaler()
scaled = scaler.fit_transform(filled)
# PCA取第一主成分
n_components = min(1, filled.shape[1], filled.shape[0])
pca = PCA(n_components=n_components)
scores = pca.fit_transform(scaled)[:, 0]
# 如果载荷为负(方向反转),翻转
loadings = pca.components_[0]
if np.sum(loadings) < 0:
scores = -scores
# 标准化到50/10
return self.standardize_scores(scores)
def determine_level(self, score: float, dimension: str) -> int:
"""根据分数和维度确定水平(1-4"""
thresholds = LEVEL_THRESHOLDS.get(dimension, {})
if not thresholds:
# 默认阈值
if score > 55:
return 4
elif score > 50:
return 3
elif score > 45:
return 2
else:
return 1
if score > thresholds["level4"]:
return 4
elif score > thresholds["level3"]:
return 3
elif score > thresholds["level2"]:
return 2
else:
return 1
def get_level_description(self, dimension: str, level: int) -> str:
"""获取水平的质性描述"""
descriptions = LEVEL_DESCRIPTIONS.get(dimension, {})
return descriptions.get(level, f"水平{level}")
def compute_dimension_scores(self, school_raw_scores: Dict[str, Dict[str, List[float]]]) -> pd.DataFrame:
"""
计算所有学校在各三级维度上的得分
school_raw_scores: {
school_name: {
sub_dimension_name: [score1, score2, ...] # 该维度下各题目的赋分
}
}
返回 DataFrame: 行=学校, 列=三级维度, 值=标准化得分
"""
schools = list(school_raw_scores.keys())
all_sub_dims = []
for dim, info in DIMENSION_FRAMEWORK.items():
all_sub_dims.extend(info["sub_dimensions"])
# 构建原始矩阵
raw_matrix = {}
for sub_dim in all_sub_dims:
values = []
for school in schools:
scores = school_raw_scores.get(school, {}).get(sub_dim, [])
values.append(np.nanmean(scores) if scores else np.nan)
raw_matrix[sub_dim] = values
raw_df = pd.DataFrame(raw_matrix, index=schools)
# PCA合成并标准化各维度
result = pd.DataFrame(index=schools)
for sub_dim in all_sub_dims:
if sub_dim in raw_df.columns:
result[sub_dim] = self.standardize_scores(raw_df[sub_dim].values)
else:
result[sub_dim] = PCA_MEAN
self._sub_dimension_scores = result
return result
def compute_dimension_aggregates(self, sub_scores: pd.DataFrame) -> pd.DataFrame:
"""
从三级维度分数聚合到二级维度(均值)
sub_scores: 行=学校, 列=三级维度
返回: 行=学校, 列=二级维度
"""
result = pd.DataFrame(index=sub_scores.index)
for dim, info in DIMENSION_FRAMEWORK.items():
sub_dims = [s for s in info["sub_dimensions"] if s in sub_scores.columns]
if sub_dims:
result[dim] = sub_scores[sub_dims].mean(axis=1)
else:
result[dim] = PCA_MEAN
# 总体得分(七维度均值)
result["总体得分"] = result[list(DIMENSION_FRAMEWORK.keys())].mean(axis=1)
self._dimension_scores = result
return result
def compute_levels(self, sub_scores: pd.DataFrame) -> pd.DataFrame:
"""计算各学校各维度的水平等级"""
levels = pd.DataFrame(index=sub_scores.index)
for col in sub_scores.columns:
levels[col] = sub_scores[col].apply(
lambda x: self.determine_level(x, col)
)
return levels
def cluster_analysis(self, scores: pd.DataFrame, n_clusters: int = 2) -> Dict:
"""
聚类分析
scores: 行=学校, 列=维度
返回: 聚类标签和各类特征
"""
# 标准化
scaler = StandardScaler()
scaled = scaler.fit_transform(scores.fillna(PCA_MEAN))
# KMeans聚类
n_clusters = min(n_clusters, len(scores))
if n_clusters < 2:
return {"labels": [0] * len(scores), "centers": scores.values.tolist()}
kmeans = KMeans(n_clusters=n_clusters, random_state=42, n_init=10)
labels = kmeans.fit_predict(scaled)
# 计算各类均值
cluster_means = {}
for c in range(n_clusters):
mask = labels == c
cluster_means[c] = scores[mask].mean().to_dict()
# 确定哪个是"较好类"(总均值更高的)
avg_per_cluster = {c: np.mean(list(v.values())) for c, v in cluster_means.items()}
sorted_clusters = sorted(avg_per_cluster.items(), key=lambda x: x[1], reverse=True)
cluster_names = {}
for rank, (c, _) in enumerate(sorted_clusters):
if rank == 0:
cluster_names[c] = "较好"
else:
cluster_names[c] = "待提升"
return {
"labels": labels.tolist(),
"school_clusters": {
school: cluster_names[labels[i]]
for i, school in enumerate(scores.index)
},
"cluster_means": cluster_means,
"cluster_names": cluster_names,
}
def t_test_vs_mean(self, school_scores: np.ndarray, ref_mean: float) -> Dict:
"""
单样本T检验:学校各学科得分 vs 参考均值
school_scores: 该学校在某维度各学科的得分
ref_mean: 参考均值(如区均值、全市均值)
"""
scores = school_scores[~np.isnan(school_scores)]
if len(scores) < 2:
return {"t": np.nan, "p": np.nan, "significant": False, "n": len(scores)}
t_stat, p_value = stats.ttest_1samp(scores, ref_mean)
return {
"t": round(float(t_stat), 3),
"p": round(float(p_value), 4),
"significant": float(p_value) < 0.05,
"n": len(scores),
}
def correlation_analysis(self, dim_scores: pd.DataFrame) -> pd.DataFrame:
"""维度间相关性分析"""
dim_cols = [c for c in dim_scores.columns if c in DIMENSION_FRAMEWORK]
return dim_scores[dim_cols].corr()
def compute_school_report_data(self, school: str,
sub_scores: pd.DataFrame,
dim_scores: pd.DataFrame) -> Dict:
"""
为某一所学校生成完整的报告数据包
返回包含所有统计分析结果的结构化数据
"""
schools = list(sub_scores.index)
if school not in schools:
raise ValueError(f"学校 '{school}' 不在数据中")
school_info = SCHOOL_TYPE_MAP.get(school, {})
school_type = school_info.get("type", "")
# 区均值
district_avg_sub = sub_scores.mean()
district_avg_dim = dim_scores.mean()
# 同类学校均值
same_type_schools = [s for s in schools if SCHOOL_TYPE_MAP.get(s, {}).get("type") == school_type]
if same_type_schools:
same_type_avg_sub = sub_scores.loc[same_type_schools].mean()
same_type_avg_dim = dim_scores.loc[same_type_schools].mean()
else:
same_type_avg_sub = district_avg_sub
same_type_avg_dim = district_avg_dim
# 水平判定
levels = self.compute_levels(sub_scores)
# 聚类(二级维度)
dim_cols = [c for c in dim_scores.columns if c in DIMENSION_FRAMEWORK]
overall_cluster = self.cluster_analysis(dim_scores[dim_cols])
# 各二级维度聚类
dim_clusters = {}
for dim, info in DIMENSION_FRAMEWORK.items():
sub_dims = [s for s in info["sub_dimensions"] if s in sub_scores.columns]
if sub_dims:
dim_clusters[dim] = self.cluster_analysis(sub_scores[sub_dims])
# 相关性
correlation = self.correlation_analysis(dim_scores)
# 构建报告数据
report = {
"school": school,
"school_info": school_info,
# 总体得分
"overall": {
"score": round(float(dim_scores.loc[school, "总体得分"]), 2),
"district_avg": round(float(district_avg_dim["总体得分"]), 2),
"same_type_avg": round(float(same_type_avg_dim.get("总体得分", PCA_MEAN)), 2),
"rank_in_district": int((dim_scores["总体得分"] >= dim_scores.loc[school, "总体得分"]).sum()),
"total_schools": len(schools),
"cluster": overall_cluster["school_clusters"].get(school, ""),
},
# 二级维度
"dimensions": {},
# 三级维度
"sub_dimensions": {},
# 相关性矩阵
"correlation": correlation.to_dict(),
# 所有学校得分(用于对比)
"all_schools_dim_scores": dim_scores.to_dict(),
"all_schools_sub_scores": sub_scores.to_dict(),
}
# 填充二级维度数据
for dim in DIMENSION_FRAMEWORK:
score = float(dim_scores.loc[school, dim])
d_avg = float(district_avg_dim[dim])
st_avg = float(same_type_avg_dim.get(dim, PCA_MEAN))
# T检验:该学校在该维度下各三级维度得分 vs 区均值
sub_dims = DIMENSION_FRAMEWORK[dim]["sub_dimensions"]
sub_vals = np.array([float(sub_scores.loc[school, s]) for s in sub_dims if s in sub_scores.columns])
t_test = self.t_test_vs_mean(sub_vals, d_avg)
report["dimensions"][dim] = {
"score": round(score, 2),
"district_avg": round(d_avg, 2),
"same_type_avg": round(st_avg, 2),
"diff_district": round(score - d_avg, 2),
"rank_in_district": int((dim_scores[dim] >= score).sum()),
"t_test_vs_district": t_test,
"cluster": dim_clusters.get(dim, {}).get("school_clusters", {}).get(school, ""),
}
# 填充三级维度数据
for dim, info in DIMENSION_FRAMEWORK.items():
for sub_dim in info["sub_dimensions"]:
if sub_dim not in sub_scores.columns:
continue
score = float(sub_scores.loc[school, sub_dim])
d_avg = float(district_avg_sub[sub_dim])
level = int(levels.loc[school, sub_dim])
# 各学校在该维度的排名
rank = int((sub_scores[sub_dim] >= score).sum())
# 水平分布统计
dim_levels = levels[sub_dim]
level_dist = {
f"水平{i}": int((dim_levels == i).sum())
for i in range(1, 5)
}
report["sub_dimensions"][sub_dim] = {
"parent_dimension": dim,
"score": round(score, 2),
"district_avg": round(d_avg, 2),
"diff_district": round(score - d_avg, 2),
"rank_in_district": rank,
"level": level,
"level_description": self.get_level_description(sub_dim, level),
"level_distribution": level_dist,
}
return report