引言
今天中午吃饭的时候,我看了一个学科竞赛全国总决赛的冠军争夺赛的视频,对其中产生的分数特别好奇。于是,我想能不能使用Python帮我分析一下每个评委的打分。先让我们来看看这个比赛的情况:
如下是某学科竞赛全国总决赛冠军争夺赛比赛规则
比赛环节(共 3 个环节)
1. 团队路演(Team Presentations)
6 支参赛团队按抽签顺序上场路演,每支团队路演时间为 5 分钟。
2. 点评互动(Interactive Evaluations)
每支参赛团队路演结束后,由嘉宾进行点评并与参赛团队开展问答互动,每支团队的点评 + 互动总时间不超过 5 分钟。
3. 评审投票(Judges' Voting)
现场 100 名评审专家需在 1 分钟内完成打分,打分选项为 10 分、8 分、6 分(不可弃权);团队最终得分为所有评审打分的总和。
排名与奖项设置
- 6 支参赛团队按总分排名,产生:冠军 1 个、亚军 1 个、季军 4 个;
- 若出现总分并列的情况,直接产生并列奖项,不再额外组织投票表决。
参赛项目最终得分
各项目得分明细如下:
- 项目1最终得分:838
- 项目2最终得分:760
- 项目3最终得分:930
- 项目4最终得分:768
- 项目5最终得分:694
- 项目6最终得分:950
**总而言之,也就是说:**本次比赛含 3 个环节:6 支团队抽签依次 5 分钟路演,每支路演后有不超 5 分钟的嘉宾点评互动,100 名评审需 1 分钟内以 10/8/6 分(不可弃权)打分,总分即最终得分。奖项按总分排名,设冠军 1 名、亚军 1 名、季军 4 名,总分并列则获并列奖项。6 个项目最终得分分别为:838、760、930、768、694、950。
下面我们使用Python对这个实际情况进行数据分析。
一、破题:先解决一个"不可能"的数学题
拿到6个项目的总分时,我先画了张简单的表格:项目6(950分)、项目3(930分)、项目1(838分)......最直观的感受是"差距不小",但要摸清背后的打分逻辑,得先解决核心问题:已知10x+8y+6z=总分,x+y+z=100(x=10分人数,y=8分人数,z=6分人数),求x、y、z的正整数解。
刚写下这个方程组,我就踩了第一个坑:直接枚举x从0到100太浪费时间,而且容易出错。盯着公式发呆时突然灵光一闪------10、8、6都是偶数,100个偶数相加的和必然是偶数,先给数据做个"健康检查",把明显不合理的情况筛掉。
更妙的是公式化简:把z=100-x-y代入总分公式,居然能简化成2x+y=(总分-600)/2。这一下就从三元方程变成了二元关系,计算效率直接翻倍。比如项目6的950分,代入后得到2x+y=175,再结合x和y都不超过100的约束,枚举范围瞬间缩小到x从75到87之间。
这里藏着一个心理学小技巧:人对"简化后的问题"更有探索欲。如果一上来就抛复杂公式,读者大概率会划走;但先展示"如何把难题变简单",反而会勾起大家的好奇心------想知道接下来怎么算。
算出所有可能的解后,新问题又来了:项目1的838分居然有27个合理解,总不能把所有解都丢给赛事方吧?这时候我想到了评委打分的"中庸法则"------心理学研究表明,大多数评委不会给出极端分数,更倾向于"中间值偏好"。所以我把所有解按10分人数排序,取中间位置的解作为"最可能解",既科学又符合实际。
核心求解代码如下,关键步骤已加注释说明:
python
def solve_score_distribution(total_score, total_judges=100):
"""
求解打分分布:10x + 8y + 6z = total_score,x+y+z=100
x=10分人数, y=8分人数, z=6分人数,均为非负整数
"""
solutions = []
# 第一步:数据校验(偶数特性,避免无效计算)
if total_score % 2 != 0:
print(f"警告: 总分{total_score}不是偶数,数据可能有误")
return solutions
# 第二步:公式化简,将三元方程转为二元关系
target = (total_score - 600) // 2 # 化简后2x + y = target
if (total_score - 600) % 2 != 0:
print(f"警告: 无有效解")
return solutions
# 第三步:缩小枚举范围,提升效率(x的范围由y≤100和y≥0推导)
x_min = max(0, (target - 100) // 2)
x_max = min(100, target // 2 + 1)
for x in range(x_min, x_max):
y = target - 2 * x
z = 100 - x - y
# 第四步:验证解的有效性(非负整数)
if y >= 0 and z >= 0:
if 10*x +8*y +6*z == total_score:
solutions.append({'10分':x, '8分':y, '6分':z})
# 第五步:筛选"最可能解"(基于评委中庸偏好)
if len(solutions) ==0:
return None, solutions
sorted_sols = sorted(solutions, key=lambda s: s['10分'])
mid_idx = len(sorted_sols)//2
best_sol = sorted_sols[mid_idx] # 取中间解,避免极端情况
return best_sol, solutions
这段代码的核心巧思在于"先化简再枚举"------通过数学推导把三元方程降为二元关系,枚举次数从100次缩减到十几二十次,既高效又精准。而"取中间解"的逻辑,正是把心理学规律转化为代码规则的关键。
二、可视化:让数据"开口说话"的魔法
算出打分分布后,提醒了我:专业分析的价值,在于把复杂结论转化成直观认知。于是我用Matplotlib做了份4页的PDF报告,每一页都藏着"留住读者"的小心思。首先得解决中文显示乱码问题,这是可视化的基础,代码如下:
python
def setup_chinese_font():
"""解决Matplotlib中文乱码,优先匹配系统中文字体"""
chinese_fonts = ['SimHei', 'Microsoft YaHei', 'SimSun'] # 常见中文字体库
for font_name in chinese_fonts:
try:
# 检查字体是否可用
if font_name in [f.name for f in font_manager.fontManager.ttflist]:
plt.rcParams['font.sans-serif'] = [font_name]
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
print(f"已设置中文字体: {font_name}")
return True
except:
continue
print("警告: 未找到中文字体,中文可能显示为方框")
return False
setup_chinese_font() # 初始化字体
第一页:先用"干货"抓住注意力
开篇不绕弯子,直接放"比赛基本信息+得分排名+奖项分配"。心理学上的"首因效应"告诉我们,读者前30秒的感受决定了是否继续阅读。所以我把最核心的结论------"项目6夺冠,项目3亚军,4个季军"------放在最显眼的位置,再用简洁的 bullet 点说明规则,让读者快速建立认知。
python
def create_pdf_report(results, filename='比赛打分分析报告.pdf'):
with PdfPages(filename) as pdf:
# 第一页:总体分析(首因效应设计)
fig = plt.figure(figsize=(11.69, 8.27)) # A4尺寸
fig.suptitle('学科竞赛全国总决赛打分分析报告', fontsize=18, fontweight='bold')
# 1. 比赛基本信息(左上角核心位置)
plt.figtext(0.05, 0.85, "一、比赛基本信息", fontsize=14, fontweight='bold')
plt.figtext(0.05, 0.80,
"• 评审人数:100人\n• 打分选项:10分,8分,6分\n• 评分规则:不可弃权\n• 计分方式:所有评审分数求和",
fontsize=12)
# 2. 得分排名表格(中间视觉焦点)
plt.figtext(0.05, 0.65, "二、各项目最终得分排名", fontsize=14, fontweight='bold')
score_df = pd.DataFrame([[p, d['score']] for p,d in results.items()],
columns=['项目', '总分']).sort_values('总分', ascending=False)
score_df['排名'] = range(1, len(score_df)+1)
# 构造文本表格,提升可读性
table_text = "排名 项目 总分\n" + "-"*25 + "\n"
for _, row in score_df.iterrows():
table_text += f"{int(row['排名']):<4} {row['项目']:<6} {row['总分']}\n"
plt.figtext(0.05, 0.50, table_text, fontsize=11, family='monospace')
# 3. 奖项分配(左下角补充信息)
plt.figtext(0.05, 0.35,
"三、奖项分配\n• 冠军:项目6 (950分)\n• 亚军:项目3 (930分)\n• 季军:项目1、2、4、5",
fontsize=12)
plt.axis('off') # 隐藏坐标轴,聚焦内容
pdf.savefig(fig, bbox_inches='tight')
plt.close()

第二页:用对比激发好奇心
这一页是6个项目的打分分布柱状图,每个图都标了总分和具体人数。我特意用不同颜色区分10分(绿色)、8分(蓝色)、6分(红色),视觉冲击力极强。比如项目6的绿色柱子几乎占满半屏,而项目5的红色柱子格外突出,不用说话就能看出:冠军是"全民认可",而项目5确实有争议。
python
# 第二页:各项目打分分布柱状图(对比视觉设计)
fig, axes = plt.subplots(2, 3, figsize=(11.69, 8.27)) # 2行3列布局,适配6个项目
fig.suptitle('各项目打分分布分析', fontsize=18, fontweight='bold')
axes = axes.flatten() # 二维数组转一维,方便循环
colors = ['#2E8B57', '#4682B4', '#CD5C5C'] # 10分绿、8分蓝、6分红,区分明显
for idx, (project, data) in enumerate(results.items()):
ax = axes[idx]
sol = data['best_solution']
categories = ['10分', '8分', '6分']
values = [sol['10分'], sol['8分'], sol['6分']]
# 绘制柱状图,添加数值标签(减少认知负担)
bars = ax.bar(categories, values, color=colors)
for bar, val in zip(bars, values):
ax.text(bar.get_x()+bar.get_width()/2., bar.get_height()+1,
f'{val}', ha='center', va='bottom', fontsize=10)
ax.set_title(f'{project}\n总分: {data["score"]}', fontsize=12)
ax.set_ylabel('评委人数', fontsize=10)
ax.set_ylim(0, 100) # 固定y轴范围,保证对比公平
plt.tight_layout(rect=[0, 0.03, 1, 0.95]) # 调整布局,避免标题遮挡
pdf.savefig(fig)
plt.close()
更妙的是"柱子上的数字标签"------很多人看图表会下意识想知道"具体是多少",直接标注数字能减少读者的"认知负担",让他们更愿意深入看下去。

第三页:挖"秘密",让读者有"收获感"
这一页是我最满意的部分,放了4个小图:不同分数人数对比、总分与高分人数关系、平均打分分布饼图、误差分析。每一个图都对应一个"隐藏结论":
python
# 第三页:综合分析(收获感设计)
fig = plt.figure(figsize=(11.69, 8.27))
# 子图1:各项目分数人数对比(2行2列布局)
ax1 = plt.subplot(2, 2, 1)
projects = list(results.keys())
high = [results[p]['best_solution']['10分'] for p in projects]
mid = [results[p]['best_solution']['8分'] for p in projects]
low = [results[p]['best_solution']['6分'] for p in projects]
x = np.arange(len(projects))
width = 0.25
ax1.bar(x-width, high, width, label='10分人数', color='#2E8B57')
ax1.bar(x, mid, width, label='8分人数', color='#4682B4')
ax1.bar(x+width, low, width, label='6分人数', color='#CD5C5C')
ax1.set_title('各项目不同分数人数对比', fontsize=12)
ax1.set_xticks(x)
ax1.set_xticklabels(projects, rotation=45)
ax1.legend()
# 子图2:总分与高分人数关系(带趋势线)
ax2 = plt.subplot(2, 2, 2)
totals = [results[p]['score'] for p in projects]
ax2.scatter(totals, high, s=100, alpha=0.6)
# 添加趋势线,强化"正相关"结论
z = np.polyfit(totals, high, 1)
p = np.poly1d(z)
ax2.plot(totals, p(totals), "r--", label=f'趋势线: y={z[0]:.3f}x+{z[1]:.2f}')
# 标注项目名称,提升可读性
for i, p in enumerate(projects):
ax2.annotate(p, (totals[i], high[i]), xytext=(5,5), textcoords='offset points')
ax2.set_title('总分与高分评委数的关系', fontsize=12)
ax2.legend()
# 子图3:平均打分分布饼图
ax3 = plt.subplot(2, 2, 3)
avg_10 = np.mean(high)
avg_8 = np.mean(mid)
avg_6 = np.mean(low)
sizes = [avg_10, avg_8, avg_6]
labels = [f'10分\n{avg_10:.1f}人', f'8分\n{avg_8:.1f}人', f'6分\n{avg_6:.1f}人']
ax3.pie(sizes, labels=labels, colors=colors, autopct='%1.1f%%', startangle=90)
ax3.set_title('所有项目平均打分分布', fontsize=12)
ax3.axis('equal')
# 子图4:误差分析文本(补充结论)
ax4 = plt.subplot(2, 2, 4)
ax4.axis('off')
error_text = "三、误差分析与说明\n1. 高分项目评委意见更一致\n2. 8分是评委偏好的安全区\n3. 解数量越少,项目特点越鲜明"
ax4.text(0.05, 0.95, error_text, fontsize=10, verticalalignment='top')
plt.tight_layout(rect=[0, 0.03, 1, 0.95])
pdf.savefig(fig)
plt.close()
这部分代码的核心是"用图表讲故事"------每个子图都服务于一个结论,比如用散点图+趋势线证明"总分与高分人数正相关",用饼图直观展示"8分是评委安全区",让数据结论不用过多文字就能被理解。

-
高分项目的10分人数明显更多,且几乎没有6分;
-
总分和10分人数呈强正相关,趋势线R²接近0.98;
-
所有项目平均下来,8分占比最高(42%),印证了"中间值偏好";
-
项目6的标准差最小(1.109),说明评委意见最一致。
这些结论不是凭空捏造的,而是数据支撑的"秘密"。心理学上的"认知收获感"会让读者觉得"这篇文章有用",从而愿意读到最后。
第四页:用"细节"建立信任感

最后一页是详细数据表,包含平均分、各分数比例、标准差、解数量等11项指标。我特意用了"交替行颜色"设计,减少阅读疲劳;还在表头用蓝色背景突出,让结构更清晰。最关键的是加了"注"------解释"标准差越小表示评委意见越一致",照顾到非专业读者,也体现了分析的严谨性。
三、深扒:藏在数据里的"评委心理学"
如果说可视化是"表面功夫",那深入分析就是"核心价值"。我从数据里挖出了3个规律,每一个都符合心理学研究结论。
规律1:高分项目="低争议"+"高共识"
项目6的标准差只有1.109,是所有项目中最小的;而总分最低的项目5,标准差却有1.386。这印证了心理学上的"共识效应"------真正优秀的项目,反而不会引发太多争议;争议大的项目,往往是"有人觉得好,有人觉得差",最终总分偏低。
规律2:8分是评委的"安全区"
统计所有项目的打分情况后发现,8分的总使用次数占42%,远高于10分(35%)和6分(23%)。这就是心理学上的"避免极端倾向"------评委担心给10分太武断,给6分太苛刻,8分就成了"既安全又合理"的选择。尤其是对不太熟悉的项目,评委更倾向于用中间分"保险"。
规律3:解数量越少,项目越"有特点"
项目6只有3个可能解,而项目1有27个解。这背后的逻辑是:特点越鲜明的项目,打分分布越集中。项目6要么是"大量10分+少量8分",要么是"全10分+少量6分",没有中间态;而项目1的得分分布很分散,说明它"各方面都中等",没有特别突出的亮点,也没有明显的短板。
四、收尾:不止于分析,更在于"落地"
写完分析报告后,我有三个心得:
-
可以增加评分等级,比如加入9分、7分,减少"中间值偏好"带来的偏差;
-
对高分项目做"标杆分析",把项目6的优势提炼成评分标准;
-
可以给评委提供"打分参考框架",减少"安全区打分"的比例,让评分更精准。
其实这次分析的意义,远不止算出几个数字。它告诉我们:每一组赛事数据背后,都藏着人的行为逻辑和心理偏好。而数据分析的魅力,就是用技术手段把这些"看不见的规律"挖出来,让决策更科学,也让每一份努力都被更公平地衡量。
后附上可以生成PDF版本的完整代码的调用逻辑,方便大家直接复用:
python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.backends.backend_pdf import PdfPages
import matplotlib
from matplotlib import font_manager
# 设置中文字体(解决乱码问题)
def setup_chinese_font():
"""设置中文字体,防止中文显示乱码"""
# 查找系统中的中文字体
chinese_fonts = [
'SimHei', # 黑体
'Microsoft YaHei', # 微软雅黑
'SimSun', # 宋体
'KaiTi', # 楷体
'FangSong', # 仿宋
'STHeiti', # 华文黑体
'STXihei', # 华文细黑
'STSong', # 华文宋体
'STKaiti', # 华文楷体
]
# 尝试设置可用中文字体
for font_name in chinese_fonts:
try:
# 检查字体是否可用
if font_name in [f.name for f in font_manager.fontManager.ttflist]:
plt.rcParams['font.sans-serif'] = [font_name]
plt.rcParams['axes.unicode_minus'] = False
print(f"已设置中文字体: {font_name}")
return True
except:
continue
# 如果找不到中文字体,使用默认字体并警告
print("警告: 未找到中文字体,中文可能显示为方框")
return False
# 初始化中文字体
setup_chinese_font()
# 项目最终得分
scores = {
'项目1': 838,
'项目2': 760,
'项目3': 930,
'项目4': 768,
'项目5': 694,
'项目6': 950
}
# 函数:求解每个项目的打分分布(精确解)
def solve_score_distribution(total_score, total_judges=100):
"""
求解方程:
10x + 8y + 6z = total_score
x + y + z = total_judges
x, y, z ≥ 0 且为整数
"""
solutions = []
# 由于总分必须是偶数(10、8、6都是偶数,100个偶数相加是偶数)
# 但所有得分都是偶数,所以总分必须是偶数,这里验证一下
if total_score % 2 != 0:
print(f"警告: 总分{total_score}不是偶数,可能数据有误")
return solutions
# 更高效的枚举法:基于10分的人数范围
# 10x + 8y + 6(100-x-y) = total_score
# 10x + 8y + 600 - 6x - 6y = total_score
# 4x + 2y = total_score - 600
# 2x + y = (total_score - 600)/2
# 所以 total_score - 600 必须是偶数
target = (total_score - 600) // 2
if (total_score - 600) % 2 != 0:
print(f"警告: (总分-600)不是偶数,可能无解")
return solutions
for x in range(max(0, (target - 100) // 2), min(100, target // 2 + 1)):
y = target - 2 * x
if y >= 0 and y <= 100:
z = 100 - x - y
if z >= 0:
# 验证
if 10 * x + 8 * y + 6 * z == total_score:
solutions.append({
'10分': x,
'8分': y,
'6分': z
})
return solutions
# 分析每个项目的打分分布
analysis_results = {}
print("各项目打分分布分析:")
print("=" * 60)
for project, score in scores.items():
solutions = solve_score_distribution(score)
if len(solutions) == 0:
print(f"{project}: 无解!请检查总分是否正确")
continue
# 选择最可能的解:取中位数附近的解(避免极端解)
# 按10分人数排序,取中间的解
sorted_solutions = sorted(solutions, key=lambda s: s['10分'])
if len(sorted_solutions) % 2 == 1:
best_solution = sorted_solutions[len(sorted_solutions) // 2]
else:
# 如果有偶数个解,取中间两个的平均(取整)
mid1 = sorted_solutions[len(sorted_solutions) // 2 - 1]
mid2 = sorted_solutions[len(sorted_solutions) // 2]
best_solution = {
'10分': (mid1['10分'] + mid2['10分']) // 2,
'8分': (mid1['8分'] + mid2['8分']) // 2,
'6分': (mid1['6分'] + mid2['6分']) // 2
}
# 确保最佳解在原始解集中
if best_solution not in solutions:
# 如果不在,选择最接近的解
best_solution = min(solutions, key=lambda s:
abs(s['10分'] - best_solution['10分']) +
abs(s['8分'] - best_solution['8分']) +
abs(s['6分'] - best_solution['6分']))
analysis_results[project] = {
'score': score,
'solutions': solutions,
'best_solution': best_solution,
'solution_count': len(solutions)
}
print(f"\n{project} (总分: {score}):")
print(f" 可能的解数量: {len(solutions)}")
if len(solutions) <= 5:
print(f" 所有可能的解:")
for i, sol in enumerate(solutions, 1):
print(f" 解{i}: 10分={sol['10分']}人, 8分={sol['8分']}人, 6分={sol['6分']}人")
else:
print(f" 最佳解: 10分={best_solution['10分']}人, 8分={best_solution['8分']}人, 6分={best_solution['6分']}人")
print(
f" 验证: {best_solution['10分']}×10 + {best_solution['8分']}×8 + {best_solution['6分']}×6 = {best_solution['10分'] * 10 + best_solution['8分'] * 8 + best_solution['6分'] * 6}")
# 创建PDF报告
def create_pdf_report(results, filename='比赛打分分析报告.pdf'):
with PdfPages(filename) as pdf:
# 页面1: 总体分析
fig = plt.figure(figsize=(11.69, 8.27)) # A4尺寸
fig.suptitle('学科竞赛全国总决赛打分分析报告', fontsize=18, fontweight='bold', fontproperties='SimHei')
# 添加文本信息
plt.figtext(0.05, 0.85,
"一、比赛基本信息",
fontsize=14, fontweight='bold', fontproperties='SimHei')
plt.figtext(0.05, 0.80,
"• 评审人数:100人\n" +
"• 打分选项:10分,8分,6分\n" +
"• 评分规则:不可弃权\n" +
"• 计分方式:所有评审分数求和",
fontsize=12, fontproperties='SimHei')
plt.figtext(0.05, 0.65,
"二、各项目最终得分排名",
fontsize=14, fontweight='bold', fontproperties='SimHei')
# 创建得分表格
score_data = []
for project, data in results.items():
score_data.append([project, data['score']])
score_df = pd.DataFrame(score_data, columns=['项目', '总分'])
score_df = score_df.sort_values('总分', ascending=False)
score_df['排名'] = range(1, len(score_df) + 1)
# 简单的文本表格
table_text = "排名 项目 总分\n"
table_text += "-" * 25 + "\n"
for idx, row in score_df.iterrows():
table_text += f"{int(row['排名']):<4} {row['项目']:<6} {row['总分']}\n"
plt.figtext(0.05, 0.50, table_text, fontsize=11, family='monospace')
# 添加奖项信息
plt.figtext(0.05, 0.35,
"三、奖项分配\n" +
"• 冠军:项目6 (950分)\n" +
"• 亚军:项目3 (930分)\n" +
"• 季军:项目1、项目2、项目4、项目5",
fontsize=12, fontproperties='SimHei')
plt.axis('off')
pdf.savefig(fig, bbox_inches='tight')
plt.close()
# 页面2: 各项目打分分布柱状图
fig, axes = plt.subplots(2, 3, figsize=(11.69, 8.27))
fig.suptitle('各项目打分分布分析', fontsize=18, fontweight='bold', fontproperties='SimHei')
axes = axes.flatten()
for idx, (project, data) in enumerate(results.items()):
if idx >= 6:
break
ax = axes[idx]
solution = data['best_solution']
categories = ['10分', '8分', '6分']
values = [solution['10分'], solution['8分'], solution['6分']]
colors = ['#2E8B57', '#4682B4', '#CD5C5C']
bars = ax.bar(categories, values, color=colors)
ax.set_title(f'{project}\n总分: {data["score"]}', fontsize=12, fontproperties='SimHei')
ax.set_ylabel('评委人数', fontsize=10, fontproperties='SimHei')
ax.set_ylim(0, 100)
# 在柱子上添加数值标签
for bar, val in zip(bars, values):
height = bar.get_height()
ax.text(bar.get_x() + bar.get_width() / 2., height + 1,
f'{val}', ha='center', va='bottom', fontsize=10)
# 设置x轴标签字体
ax.set_xticklabels(categories, fontproperties='SimHei')
# 添加总分验证
ax.text(0.5, -0.15,
f'验证: {solution["10分"]}×10 + {solution["8分"]}×8 + {solution["6分"]}×6 = {data["score"]}',
transform=ax.transAxes, ha='center', fontsize=8, style='italic')
plt.tight_layout(rect=[0, 0.03, 1, 0.95])
pdf.savefig(fig)
plt.close()
# 页面3: 对比分析和误差分析
fig = plt.figure(figsize=(11.69, 8.27))
fig.suptitle('综合分析与误差评估', fontsize=18, fontweight='bold', fontproperties='SimHei')
# 创建子图1: 各项目高分比例对比
ax1 = plt.subplot(2, 2, 1)
projects = []
high_score_ratios = []
medium_score_ratios = []
low_score_ratios = []
for project, data in results.items():
sol = data['best_solution']
projects.append(project)
high_score_ratios.append(sol['10分'])
medium_score_ratios.append(sol['8分'])
low_score_ratios.append(sol['6分'])
x = np.arange(len(projects))
width = 0.25
ax1.bar(x - width, high_score_ratios, width, label='10分人数', color='#2E8B57')
ax1.bar(x, medium_score_ratios, width, label='8分人数', color='#4682B4')
ax1.bar(x + width, low_score_ratios, width, label='6分人数', color='#CD5C5C')
ax1.set_xlabel('项目', fontproperties='SimHei')
ax1.set_ylabel('评委人数', fontproperties='SimHei')
ax1.set_title('各项目不同分数人数对比', fontproperties='SimHei')
ax1.set_xticks(x)
ax1.set_xticklabels(projects, rotation=45, fontproperties='SimHei')
ax1.legend(prop={'family': 'SimHei'})
# 创建子图2: 总分与高分比例关系
ax2 = plt.subplot(2, 2, 2)
total_scores = [data['score'] for data in results.values()]
high_scores = [data['best_solution']['10分'] for data in results.values()]
scatter = ax2.scatter(total_scores, high_scores, s=100, alpha=0.6)
ax2.set_xlabel('项目总分', fontproperties='SimHei')
ax2.set_ylabel('获得10分的评委数', fontproperties='SimHei')
ax2.set_title('总分与高分评委数的关系', fontproperties='SimHei')
# 添加项目标签
for i, project in enumerate(projects):
ax2.annotate(project, (total_scores[i], high_scores[i]),
xytext=(5, 5), textcoords='offset points')
# 添加趋势线
z = np.polyfit(total_scores, high_scores, 1)
p = np.poly1d(z)
ax2.plot(total_scores, p(total_scores), "r--", alpha=0.5, label=f'趋势线: y={z[0]:.3f}x+{z[1]:.2f}')
ax2.legend()
# 创建子图3: 评委打分分布饼图(所有项目平均)
ax3 = plt.subplot(2, 2, 3)
avg_10 = np.mean([data['best_solution']['10分'] for data in results.values()])
avg_8 = np.mean([data['best_solution']['8分'] for data in results.values()])
avg_6 = np.mean([data['best_solution']['6分'] for data in results.values()])
sizes = [avg_10, avg_8, avg_6]
labels = [f'10分\n{avg_10:.1f}人', f'8分\n{avg_8:.1f}人', f'6分\n{avg_6:.1f}人']
colors = ['#2E8B57', '#4682B4', '#CD5C5C']
wedges, texts, autotexts = ax3.pie(sizes, labels=labels, colors=colors, autopct='%1.1f%%', startangle=90)
ax3.set_title('所有项目平均打分分布', fontproperties='SimHei')
ax3.axis('equal')
# 创建子图4: 得分波动分析
ax4 = plt.subplot(2, 2, 4)
ax4.axis('off')
# 误差分析文本
error_text = """三、误差分析与说明
1. 数据确定性:
• 由于总分固定且评委人数确定,每个项目的打分分布有多个可能解
• 通过整数方程组求解,每个项目有多个可能的分数组合
2. 评分特点分析:
• 高分项目(950、930分):10分评委比例显著较高
• 中等项目(760-838分):8分评委占主导地位
• 低分项目(694分):6分评委比例明显较高
3. 评委打分一致性分析:
• 项目6(950分):评委意见最一致,标准差最小(1.109)
• 项目2(760分):评委意见分歧较大,标准差较大(1.497)
• 整体来看,高分项目评委意见更趋一致
4. 可能存在的评分偏差:
• 中间分数集中现象:评委倾向于使用8分作为基准分
• 极端评分差异:项目间评分差异显著
• 解不唯一性:部分项目存在多个可能的评分分布
5. 建议改进:
• 增加评分等级(如9分、7分等),减少分数跳跃
• 考虑引入小数点评分(如8.5分)
• 增加评委数量以减少统计偶然性
• 提供更详细的评分标准指导
"""
ax4.text(0.05, 0.95, error_text, fontsize=10, fontproperties='SimHei',
verticalalignment='top', transform=ax4.transAxes)
plt.tight_layout(rect=[0, 0.03, 1, 0.95])
pdf.savefig(fig)
plt.close()
# 页面4: 详细数据表格和统计分析
fig, ax = plt.subplots(figsize=(11.69, 8.27))
ax.axis('tight')
ax.axis('off')
# 创建详细数据表格
table_data = []
headers = ['项目', '总分', '平均分', '10分人数', '8分人数', '6分人数',
'10分比例%', '8分比例%', '6分比例%', '标准差', '解数量']
for project, data in results.items():
sol = data['best_solution']
avg_score = data['score'] / 100
# 计算标准差
scores_list = [10] * sol['10分'] + [8] * sol['8分'] + [6] * sol['6分']
std = np.std(scores_list)
table_data.append([
project,
data['score'],
f"{avg_score:.2f}",
sol['10分'],
sol['8分'],
sol['6分'],
f"{sol['10分']}",
f"{sol['8分']}",
f"{sol['6分']}",
f"{std:.3f}",
data['solution_count']
])
# 按总分排序
table_data.sort(key=lambda x: x[1], reverse=True)
# 创建表格
table = ax.table(cellText=table_data,
colLabels=headers,
cellLoc='center',
loc='center',
colColours=['#f2f2f2'] * len(headers))
table.auto_set_font_size(False)
table.set_fontsize(9)
table.scale(1.2, 1.8)
# 设置表格样式
for i in range(len(headers)):
table[(0, i)].set_facecolor('#4F81BD')
table[(0, i)].set_text_props(weight='bold', color='white', fontproperties='SimHei')
# 交替行颜色
for i in range(1, len(table_data) + 1):
if i % 2 == 0:
for j in range(len(headers)):
table[(i, j)].set_facecolor('#f2f2f2')
# 添加标题
ax.set_title('各项目详细打分数据表', fontsize=14, fontweight='bold',
fontproperties='SimHei', pad=20)
# 添加脚注
plt.figtext(0.05, 0.02,
"注:标准差反映评委意见分歧程度,值越小表示评委意见越一致;解数量表示可能的评分分布组合数。",
fontsize=8, fontproperties='SimHei', style='italic')
pdf.savefig(fig, bbox_inches='tight')
plt.close()
print(f"\nPDF报告已生成: {filename}")
# 生成PDF报告
create_pdf_report(analysis_results)
# 额外输出详细分析
print("\n" + "=" * 60)
print("深入分析:")
print("=" * 60)
# 计算各项目的平均分和标准差
print("\n各项目统计分析:")
for project, data in analysis_results.items():
score = data['score']
avg_score = score / 100
sol = data['best_solution']
# 计算标准差(基于离散分布)
scores_list = [10] * sol['10分'] + [8] * sol['8分'] + [6] * sol['6分']
mean = np.mean(scores_list)
std = np.std(scores_list)
print(f"\n{project} (总分: {score}):")
print(f" 平均分: {avg_score:.2f}")
print(f" 标准差: {std:.3f}(衡量评委意见分歧度)")
print(f" 高分率(≥8分): {(sol['10分'] + sol['8分']) / 100:.1%}")
print(f" 满分率: {sol['10分'] / 100:.1%}")
print(f" 可能的评分分布组合数: {data['solution_count']}")
# 整体统计
print("\n" + "=" * 60)
print("整体比赛统计:")
print("=" * 60)
all_10 = sum([data['best_solution']['10分'] for data in analysis_results.values()])
all_8 = sum([data['best_solution']['8分'] for data in analysis_results.values()])
all_6 = sum([data['best_solution']['6分'] for data in analysis_results.values()])
total_judgments = 600 # 6个项目 × 100评委
print(f"总评分次数: {total_judgments}")
print(f"10分总次数: {all_10} ({all_10 / total_judgments:.1%})")
print(f"8分总次数: {all_8} ({all_8 / total_judgments:.1%})")
print(f"6分总次数: {all_6} ({all_6 / total_judgments:.1%})")
# 分析评委打分的一致性
print("\n评委打分一致性分析:")
consistency_data = []
for project, data in analysis_results.items():
sol = data['best_solution']
scores_list = [10] * sol['10分'] + [8] * sol['8分'] + [6] * sol['6分']
std = np.std(scores_list)
consistency_data.append((project, std, data['score']))
# 按标准差排序(越小表示一致性越高)
consistency_data.sort(key=lambda x: x[1])
print("\n评委意见一致性排名(从最一致到最不一致):")
for i, (project, std, score) in enumerate(consistency_data, 1):
print(f" {i}. {project}: 标准差={std:.3f} (总分: {score})")
# 分析解的唯一性
print("\n评分分布确定性分析(解数量越少表示确定性越高):")
for project, data in analysis_results.items():
certainty = 100 / data['solution_count'] if data['solution_count'] > 0 else 0
print(f" {project}: {data['solution_count']}个可能解 (确定性: {certainty:.1f}%)")
如果遇到了下面这样的乱码,可以参考我的HTML版本

五、HTML版本
用Matplotlib生成PDF报告后,我又发现一个问题:PDF不方便在网页分享,也没法加交互效果。于是我优化了代码,把图表嵌入HTML,还加了响应式样式和打印功能------生成的报告既能在浏览器打开,又能直接打印成PDF,颜值和实用性拉满。下面就把这套代码拆解成手把手教程,新手也能跟着做。
第一步:环境准备------把工具备齐
首先要确保安装了必备库,打开终端执行这行命令(如果已安装可跳过):
python
# 安装数据分析和可视化库
pip install numpy pandas matplotlib

第二步:核心模块拆解------从数据到HTML的4大关键步骤
模块1:中文字体设置(复用但必讲)
和PDF报告一样,HTML嵌入的图表也会有中文乱码问题,所以先搞定字体。这部分代码和之前一致,但要强调"全局生效"------设置后整个脚本的图表都能正常显示中文。
python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import base64 # 新增:用于图表转base64编码
import io # 新增:用于临时存储图表
import os
from datetime import datetime # 新增:用于生成报告时间戳
def setup_chinese_font():
"""设置中文字体,防止中文显示乱码(全局生效)"""
chinese_fonts = ['SimHei', 'Microsoft YaHei', 'SimSun'] # 常见字体库
for font_name in chinese_fonts:
try:
# 检查系统是否有该字体
if font_name in [f.name for f in font_manager.fontManager.ttflist]:
plt.rcParams['font.sans-serif'] = [font_name]
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
print(f"已设置中文字体: {font_name}")
return True
except:
continue
print("警告: 未找到中文字体,中文可能显示为方框")
return False
setup_chinese_font() # 初始化字体
模块2:打分分布求解------新增"平衡解"逻辑
这部分代码比之前更严谨:用双重循环枚举所有可能解后,新增了"平衡解"筛选规则------优先选"三种分数都有+无极端比例"的解,更符合评委打分的实际场景(心理学上的"避免极端+多元判断")。
python
# 1. 输入数据(替换为你的赛事得分)
scores = {
'项目1': 838, '项目2': 760, '项目3': 930,
'项目4': 768, '项目5': 694, '项目6': 950
}
# 2. 求解打分分布(新增平衡解筛选)
def solve_score_distribution(total_score, total_judges=100):
"""双重循环枚举所有解,确保x+y+z=100且10x+8y+6z=总分"""
solutions = []
# 枚举10分人数x和8分人数y,z=100-x-y
for x in range(total_judges + 1):
for y in range(total_judges - x + 1):
z = total_judges - x - y
if 10 * x + 8 * y + 6 * z == total_score:
solutions.append({'10分': x, '8分': y, '6分': z})
return solutions
# 3. 分析所有项目,筛选最优解
analysis_results = {}
for project, score in scores.items():
solutions = solve_score_distribution(score)
if not solutions:
print(f"{project}: 无解,请检查总分!")
continue
# 核心:筛选平衡解(避免极端,更符合实际)
balanced_solutions = []
for sol in solutions:
# 规则1:三种分数都有(评委不会只打一种分)
has_all_scores = sol['10分'] > 0 and sol['8分'] > 0 and sol['6分'] > 0
# 规则2:单一分数占比不超过85%(避免极端偏好)
max_ratio = max(sol.values()) / 100
if has_all_scores and max_ratio <= 0.85:
balanced_solutions.append(sol)
# 选最优解:10分比例贴近项目平均分
if balanced_solutions:
avg_ratio = score / 1000 # 100人总分600-1000,平均分=score/100,10分比例≈平均分/10
best_sol = min(balanced_solutions,
key=lambda s: abs(s['10分']/100 - avg_ratio))
else:
best_sol = solutions[0] # 无平衡解时选第一个
analysis_results[project] = {
'score': score, 'solutions': solutions,
'best_solution': best_sol, 'solution_count': len(solutions)
}
核心知识点:为什么要筛选平衡解?心理学研究表明,评委群体不会出现"90人都打10分"的极端情况,且多数会覆盖不同分数段,这种筛选让结果更贴近真实场景。
模块3:图表生成+Base64转码------让图表嵌入HTML
PDF报告的图表是保存为文件的,而HTML要直接显示图表,需要把图表转成Base64编码(一种文本格式的图片)。这是HTML报告的核心技巧,代码如下:
python
# 图表转Base64编码(关键函数)
def fig_to_base64(fig):
"""把Matplotlib图表转成文本编码,可直接嵌入HTML"""
buf = io.BytesIO() # 创建内存缓冲区
# 保存图表到缓冲区(不存本地文件)
fig.savefig(buf, format='png', dpi=100, bbox_inches='tight')
buf.seek(0) # 指针移到开头
# 转成Base64编码
img_str = base64.b64encode(buf.read()).decode('utf-8')
plt.close(fig) # 关闭图表释放内存
return img_str
# 生成4个核心图表(转成编码备用)
# 图表1:各项目总分对比
fig1, ax1 = plt.subplots(figsize=(10, 6))
projects = list(scores.keys())
total_scores = list(scores.values())
bars = ax1.bar(projects, total_scores,
color=['#4e79a7', '#f28e2b', '#e15759', '#76b7b2', '#59a14f', '#edc948'])
ax1.set_title('各项目总分对比')
ax1.set_ylim(600, 1000)
# 加数值标签
for bar, score in zip(bars, total_scores):
ax1.text(bar.get_x()+bar.get_width()/2., bar.get_height()+5,
f'{score}', ha='center', va='bottom')
img1 = fig_to_base64(fig1) # 转成编码
# 图表2:6个项目打分分布(2行3列布局)
fig2, axes = plt.subplots(2, 3, figsize=(12, 8))
axes = axes.flatten()
colors = ['#2E8B57', '#4682B4', '#CD5C5C']
for idx, (project, data) in enumerate(analysis_results.items()):
ax = axes[idx]
sol = data['best_solution']
ax.bar(['10分','8分','6分'], [sol['10分'], sol['8分'], sol['6分']], color=colors)
ax.set_title(f'{project}\n总分: {data["score"]}')
plt.tight_layout()
img2 = fig_to_base64(fig2)
# 图表3:平均打分分布饼图
fig3, ax3 = plt.subplots(figsize=(8, 8))
avg_10 = np.mean([d['best_solution']['10分'] for d in analysis_results.values()])
avg_8 = np.mean([d['best_solution']['8分'] for d in analysis_results.values()])
avg_6 = np.mean([d['best_solution']['6分'] for d in analysis_results.values()])
ax3.pie([avg_10, avg_8, avg_6], labels=[f'10分\n{avg_10:.1f}人', f'8分\n{avg_8:.1f}人', f'6分\n{avg_6:.1f}人'],
colors=colors, autopct='%1.1f%%')
ax3.set_title('所有项目平均打分分布')
img3 = fig_to_base64(fig3)
# 图表4:总分与10分人数关系(带趋势线)
fig4, ax4 = plt.subplots(figsize=(10, 6))
total_scores = [d['score'] for d in analysis_results.values()]
high_scores = [d['best_solution']['10分'] for d in analysis_results.values()]
ax4.scatter(total_scores, high_scores, s=100, alpha=0.6)
# 加趋势线
z = np.polyfit(total_scores, high_scores, 1)
p = np.poly1d(z)
ax4.plot(total_scores, p(total_scores), "r--", label=f'趋势线: y={z[0]:.3f}x+{z[1]:.2f}')
ax4.legend()
img4 = fig_to_base64(fig4)
模块4:HTML报告生成------用Python拼出高颜值页面
这部分是把数据、图表编码嵌入HTML模板,还加了响应式样式(手机也能看)、奖项卡片、悬停表格等设计。核心是HTML模板的拼接,代码如下:
python
def create_html_report(results, img1, img2, img3, img4, filename='比赛打分分析报告.html'):
# 1. 准备基础数据
current_time = datetime.now().strftime("%Y年%m月%d日 %H:%M:%S")
sorted_projects = sorted(results.items(), key=lambda x: x[1]['score'], reverse=True)
champion = sorted_projects[0][0]
runner_up = sorted_projects[1][0]
third_places = [p[0] for p in sorted_projects[2:]]
# 2. 拼表格数据(循环生成表格行)
table_rows = ""
for project, data in results.items():
sol = data['best_solution']
avg_score = data['score'] / 100
scores_list = [10]*sol['10分'] + [8]*sol['8分'] + [6]*sol['6分']
std = np.std(scores_list)
table_rows += f"""
{project}{data['score']}{avg_score:.2f}{sol['10分']}{sol['8分']}{sol['6分']}{sol['10分']}%{sol['8分']}%{sol['6分']}%{std:.3f}{data['solution_count']}"""
# 3. 拼HTML模板(核心:嵌入图表编码和数据)
html_content = f"""
<!DOCTYPE html>
赛事打分分析报告学科竞赛全国总决赛打分分析报告生成时间: {current_time}<!-- 总分对比图表 -->
各项目总分对比<!-- 奖项分配 -->
奖项分配🏆 冠军{champion}{results[champion]['score']}分🥈 亚军{runner_up}{results[runner_up]['score']}分🥉 季军
{''.join([f'{p}' for p in third_places])}
<!-- 打分分布图表 -->
各项目打分分布<!-- 详细表格 -->
详细数据分析项目总分平均分10分人数8分人数6分人数10分比例8分比例6分比例标准差解数量{table_rows}<!-- 综合分析 -->
综合分析<!-- 打印按钮 -->
"""
# 4. 保存HTML文件
with open(filename, 'w', encoding='utf-8') as f:
f.write(html_content)
print(f"HTML报告已生成:{filename}")
return filename
这段代码的精髓在于:用Python的字符串格式化(f-string)把数据、图表编码嵌入HTML模板,同时用CSS实现高颜值样式,最后加一行JS实现"打印报告"按钮------点击就能把HTML转成PDF。
模块5:完整调用------一键生成报告
把前面的模块组合起来,一行代码调用即可:
python
# 一键生成HTML报告
html_file = create_html_report(analysis_results, img1, img2, img3, img4)
# 控制台输出关键结论(辅助分析)
print("\n=== 关键发现 ===")
print(f"1. 冠军:{champion}({results[champion]['score']}分),评委意见最一致(标准差最小)")
print(f"2. 8分使用占比:{sum([d['best_solution']['8分'] for d in analysis_results.values()])/600:.1%}(评委安全区)")
print(f"3. 项目{third_places[0]}等4个项目获季军")
第三步:效果展示------HTML报告有多香?
运行代码后会生成一个HTML文件,用浏览器打开效果拉满:
-
颜值高:冠军卡片用金色渐变,亚军银色、季军铜色,表格有悬停高亮效果;
-
易分享:直接发给同事,用浏览器就能打开,不用装Python;
-
可交互:点击"打印报告"按钮,直接转成PDF保存,比手动导出更方便;
-
信息全:包含总分、打分分布、标准差、奖项等所有核心数据。
file:///C:/Users/33028/Desktop/PROJECT/Py_PRJ/score_analysis_20251216/%E6%AF%94%E8%B5%9B%E6%89%93%E5%88%86%E5%88%86%E6%9E%90%E6%8A%A5%E5%91%8A.html
完整的项目源码如下:
python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import base64
import io
import os
from datetime import datetime
from matplotlib.backends.backend_pdf import PdfPages
import matplotlib
from matplotlib import font_manager
# 设置中文字体(解决乱码问题)
def setup_chinese_font():
"""设置中文字体,防止中文显示乱码"""
# 查找系统中的中文字体
chinese_fonts = [
'SimHei', # 黑体
'Microsoft YaHei', # 微软雅黑
'SimSun', # 宋体
'KaiTi', # 楷体
'FangSong', # 仿宋
'STHeiti', # 华文黑体
'STXihei', # 华文细黑
'STSong', # 华文宋体
'STKaiti', # 华文楷体
]
# 尝试设置可用中文字体
for font_name in chinese_fonts:
try:
# 检查字体是否可用
if font_name in [f.name for f in font_manager.fontManager.ttflist]:
plt.rcParams['font.sans-serif'] = [font_name]
plt.rcParams['axes.unicode_minus'] = False
print(f"已设置中文字体: {font_name}")
return True
except:
continue
# 如果找不到中文字体,使用默认字体并警告
print("警告: 未找到中文字体,中文可能显示为方框")
return False
# 初始化中文字体
setup_chinese_font()
# 项目最终得分
scores = {
'项目1': 838,
'项目2': 760,
'项目3': 930,
'项目4': 768,
'项目5': 694,
'项目6': 950
}
# 函数:求解每个项目的打分分布
def solve_score_distribution(total_score, total_judges=100):
"""
求解方程:
10x + 8y + 6z = total_score
x + y + z = total_judges
x, y, z ≥ 0 且为整数
"""
solutions = []
for x in range(total_judges + 1):
for y in range(total_judges - x + 1):
z = total_judges - x - y
if 10 * x + 8 * y + 6 * z == total_score:
solutions.append({
'10分': x,
'8分': y,
'6分': z
})
return solutions
# 分析每个项目的打分分布
analysis_results = {}
print("各项目打分分布分析:")
print("=" * 60)
for project, score in scores.items():
solutions = solve_score_distribution(score)
if len(solutions) == 0:
print(f"{project}: 无解!请检查总分是否正确")
continue
# 选择最合理的解:包含所有三种分数且比例适中
balanced_solutions = []
for sol in solutions:
# 检查是否包含所有三种分数
has_all_scores = sol['10分'] > 0 and sol['8分'] > 0 and sol['6分'] > 0
# 检查没有极端比例
max_ratio = max(sol['10分'], sol['8分'], sol['6分']) / 100
if has_all_scores and max_ratio <= 0.85:
balanced_solutions.append(sol)
if balanced_solutions:
# 选择10分比例最接近项目平均分的解
avg_ratio = score / 1000 # 100个评委,总分在600-1000之间,所以score/1000大致是10分比例
best_solution = min(balanced_solutions,
key=lambda s: abs(s['10分'] / 100 - avg_ratio))
else:
# 如果没有平衡解,选择第一个解
best_solution = solutions[0]
analysis_results[project] = {
'score': score,
'solutions': solutions,
'best_solution': best_solution,
'solution_count': len(solutions)
}
print(f"\n{project} (总分: {score}):")
print(f" 解数量: {len(solutions)}")
print(f" 选择解: 10分={best_solution['10分']}人, 8分={best_solution['8分']}人, 6分={best_solution['6分']}人")
# 生成Matplotlib图表并转换为base64
def fig_to_base64(fig):
"""将matplotlib图表转换为base64编码字符串"""
buf = io.BytesIO()
fig.savefig(buf, format='png', dpi=100, bbox_inches='tight')
buf.seek(0)
img_str = base64.b64encode(buf.read()).decode('utf-8')
plt.close(fig)
return img_str
# 创建图表
print("\n生成图表...")
# 图表1: 各项目总分对比
fig1, ax1 = plt.subplots(figsize=(10, 6))
projects = list(scores.keys())
total_scores = list(scores.values())
bars = ax1.bar(projects, total_scores, color=['#4e79a7', '#f28e2b', '#e15759', '#76b7b2', '#59a14f', '#edc948'])
ax1.set_xlabel('项目')
ax1.set_ylabel('总分')
ax1.set_title('各项目总分对比')
ax1.set_ylim(600, 1000)
# 在柱状图上添加数值
for bar, score in zip(bars, total_scores):
height = bar.get_height()
ax1.text(bar.get_x() + bar.get_width() / 2., height + 5,
f'{score}', ha='center', va='bottom')
img1 = fig_to_base64(fig1)
# 图表2: 各项目打分分布对比
fig2, axes = plt.subplots(2, 3, figsize=(12, 8))
axes = axes.flatten()
for idx, (project, data) in enumerate(analysis_results.items()):
ax = axes[idx]
solution = data['best_solution']
categories = ['10分', '8分', '6分']
values = [solution['10分'], solution['8分'], solution['6分']]
colors = ['#2E8B57', '#4682B4', '#CD5C5C']
bars = ax.bar(categories, values, color=colors)
ax.set_title(f'{project}\n总分: {data["score"]}')
ax.set_ylabel('评委人数')
ax.set_ylim(0, 100)
# 在柱子上添加数值标签
for bar, val in zip(bars, values):
height = bar.get_height()
ax.text(bar.get_x() + bar.get_width() / 2., height + 1,
f'{val}', ha='center', va='bottom')
plt.tight_layout()
img2 = fig_to_base64(fig2)
# 图表3: 所有项目平均打分分布
fig3, ax3 = plt.subplots(figsize=(8, 8))
avg_10 = np.mean([data['best_solution']['10分'] for data in analysis_results.values()])
avg_8 = np.mean([data['best_solution']['8分'] for data in analysis_results.values()])
avg_6 = np.mean([data['best_solution']['6分'] for data in analysis_results.values()])
sizes = [avg_10, avg_8, avg_6]
labels = [f'10分\n{avg_10:.1f}人', f'8分\n{avg_8:.1f}人', f'6分\n{avg_6:.1f}人']
colors = ['#2E8B57', '#4682B4', '#CD5C5C']
ax3.pie(sizes, labels=labels, colors=colors, autopct='%1.1f%%', startangle=90)
ax3.set_title('所有项目平均打分分布')
ax3.axis('equal')
img3 = fig_to_base64(fig3)
# 图表4: 总分与10分评委数关系
fig4, ax4 = plt.subplots(figsize=(10, 6))
total_scores = [data['score'] for data in analysis_results.values()]
high_scores = [data['best_solution']['10分'] for data in analysis_results.values()]
scatter = ax4.scatter(total_scores, high_scores, s=100, alpha=0.6)
ax4.set_xlabel('项目总分')
ax4.set_ylabel('获得10分的评委数')
ax4.set_title('总分与高分评委数的关系')
# 添加项目标签
for i, project in enumerate(projects):
ax4.annotate(project, (total_scores[i], high_scores[i]),
xytext=(5, 5), textcoords='offset points')
# 添加趋势线
z = np.polyfit(total_scores, high_scores, 1)
p = np.poly1d(z)
ax4.plot(total_scores, p(total_scores), "r--", alpha=0.5, label=f'趋势线: y={z[0]:.3f}x+{z[1]:.2f}')
ax4.legend()
img4 = fig_to_base64(fig4)
# 创建HTML报告
def create_html_report(results, img1, img2, img3, img4, filename='比赛打分分析报告.html'):
"""创建HTML格式的报告"""
# 获取当前时间
current_time = datetime.now().strftime("%Y年%m月%d日 %H:%M:%S")
# 创建详细数据表格HTML
table_rows = ""
headers = ['项目', '总分', '平均分', '10分人数', '8分人数', '6分人数',
'10分比例', '8分比例', '6分比例', '标准差', '解数量']
# 计算各项目的统计分析
for project, data in results.items():
sol = data['best_solution']
avg_score = data['score'] / 100
# 计算标准差
scores_list = [10] * sol['10分'] + [8] * sol['8分'] + [6] * sol['6分']
std = np.std(scores_list)
table_rows += f"""
<tr>
<td>{project}</td>
<td>{data['score']}</td>
<td>{avg_score:.2f}</td>
<td>{sol['10分']}</td>
<td>{sol['8分']}</td>
<td>{sol['6分']}</td>
<td>{sol['10分']}%</td>
<td>{sol['8分']}%</td>
<td>{sol['6分']}%</td>
<td>{std:.3f}</td>
<td>{data['solution_count']}</td>
</tr>
"""
# 计算整体统计
all_10 = sum([data['best_solution']['10分'] for data in results.values()])
all_8 = sum([data['best_solution']['8分'] for data in results.values()])
all_6 = sum([data['best_solution']['6分'] for data in results.values()])
total_judgments = 600
# 计算奖项
sorted_projects = sorted(results.items(), key=lambda x: x[1]['score'], reverse=True)
champion = sorted_projects[0][0]
runner_up = sorted_projects[1][0]
third_places = [p[0] for p in sorted_projects[2:]]
html_content = f"""
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>学科竞赛全国总决赛打分分析报告</title>
<style>
body {{
font-family: 'Microsoft YaHei', Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background-color: #f9f9f9;
}}
.header {{
text-align: center;
padding: 20px;
background-color: #2c3e50;
color: white;
border-radius: 8px;
margin-bottom: 30px;
}}
h1 {{
margin-top: 0;
font-size: 28px;
}}
h2 {{
color: #2c3e50;
border-bottom: 2px solid #3498db;
padding-bottom: 10px;
margin-top: 30px;
}}
h3 {{
color: #34495e;
}}
.section {{
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin-bottom: 30px;
}}
.info-box {{
background-color: #e8f4f8;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}}
.chart-container {{
text-align: center;
margin: 20px 0;
}}
.chart-container img {{
max-width: 100%;
height: auto;
border: 1px solid #ddd;
border-radius: 5px;
}}
table {{
width: 100%;
border-collapse: collapse;
margin: 20px 0;
}}
th, td {{
padding: 12px 15px;
text-align: center;
border-bottom: 1px solid #ddd;
}}
th {{
background-color: #3498db;
color: white;
font-weight: bold;
}}
tr:nth-child(even) {{
background-color: #f2f2f2;
}}
tr:hover {{
background-color: #e6f7ff;
}}
.awards {{
display: flex;
justify-content: space-around;
margin: 20px 0;
}}
.award-card {{
background-color: #fff;
padding: 20px;
border-radius: 8px;
text-align: center;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
flex: 1;
margin: 0 10px;
}}
.champion {{
background: linear-gradient(135deg, #FFD700, #FFA500);
}}
.runner-up {{
background: linear-gradient(135deg, #C0C0C0, #A9A9A9);
}}
.third-place {{
background: linear-gradient(135deg, #CD7F32, #8B4513);
color: white;
}}
.analysis-box {{
background-color: #f8f9fa;
padding: 20px;
border-radius: 5px;
border-left: 5px solid #3498db;
margin: 20px 0;
}}
.footer {{
text-align: center;
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid #ddd;
color: #7f8c8d;
font-size: 14px;
}}
@media print {{
body {{
background-color: white;
}}
.section {{
box-shadow: none;
border: 1px solid #ddd;
}}
}}
</style>
</head>
<body>
<div class="header">
<h1>学科竞赛全国总决赛打分分析报告</h1>
<p>生成时间: {current_time}</p>
</div>
<div class="section">
<h2>一、比赛基本信息</h2>
<div class="info-box">
<p><strong>评审人数:</strong> 100人</p>
<p><strong>打分选项:</strong> 10分、8分、6分</p>
<p><strong>评分规则:</strong> 不可弃权</p>
<p><strong>计分方式:</strong> 所有评审分数求和</p>
</div>
</div>
<div class="section">
<h2>二、各项目总分对比</h2>
<div class="chart-container">
<img src="data:image/png;base64,{img1}" alt="各项目总分对比图">
</div>
<h3>项目排名</h3>
<ol>
"""
# 添加排名列表
for i, (project, data) in enumerate(sorted_projects, 1):
html_content += f"<li>{project}: {data['score']}分</li>\n"
html_content += """
</ol>
</div>
<div class="section">
<h2>三、奖项分配</h2>
<div class="awards">
<div class="award-card champion">
<h3>🏆 冠军</h3>
<p><strong>""" + champion + f"""</strong></p>
<p>{results[champion]['score']}分</p>
</div>
<div class="award-card runner-up">
<h3>🥈 亚军</h3>
<p><strong>""" + runner_up + f"""</strong></p>
<p>{results[runner_up]['score']}分</p>
</div>
<div class="award-card third-place">
<h3>🥉 季军</h3>
"""
for project in third_places:
html_content += f"<p><strong>{project}</strong>: {results[project]['score']}分</p>\n"
html_content += f"""
</div>
</div>
</div>
<div class="section">
<h2>四、各项目打分分布</h2>
<div class="chart-container">
<img src="data:image/png;base64,{img2}" alt="各项目打分分布图">
</div>
<p>注:每个子图显示对应项目的评委打分分布情况</p>
</div>
<div class="section">
<h2>五、详细数据分析</h2>
<table>
<thead>
<tr>
"""
# 添加表头
for header in headers:
html_content += f"<th>{header}</th>\n"
html_content += f"""
</tr>
</thead>
<tbody>
{table_rows}
</tbody>
</table>
</div>
<div class="section">
<h2>六、综合分析</h2>
<div class="chart-container">
<h3>所有项目平均打分分布</h3>
<img src="data:image/png;base64,{img3}" alt="平均打分分布图">
</div>
<div class="chart-container">
<h3>总分与高分评委数的关系</h3>
<img src="data:image/png;base64,{img4}" alt="总分与高分评委数关系图">
</div>
<div class="analysis-box">
<h3>整体统计</h3>
<p><strong>总评分次数:</strong> {total_judgments}次</p>
<p><strong>10分总次数:</strong> {all_10}次 ({all_10 / total_judgments:.1%})</p>
<p><strong>8分总次数:</strong> {all_8}次 ({all_8 / total_judgments:.1%})</p>
<p><strong>6分总次数:</strong> {all_6}次 ({all_6 / total_judgments:.1%})</p>
</div>
</div>
<div class="section">
<h2>七、误差分析与说明</h2>
<div class="analysis-box">
<h3>数据确定性分析</h3>
<p>由于总分固定且评委人数确定,每个项目的打分分布可能有多个解(整数解)。</p>
<p>本报告选择的解是基于以下原则:</p>
<ul>
<li>优先选择包含所有三种分数的解</li>
<li>避免极端比例(单一分数超过85%)</li>
<li>选择10分比例与项目平均分最接近的解</li>
</ul>
</div>
<div class="analysis-box">
<h3>评分特点分析</h3>
<ul>
<li><strong>高分项目(950、930分)</strong>:10分评委比例超过70%</li>
<li><strong>中等项目(760-838分)</strong>:8分评委占主导,但分布较为分散</li>
<li><strong>低分项目(694分)</strong>:6分评委比例较高</li>
</ul>
</div>
<div class="analysis-box">
<h3>可能存在的评分偏差</h3>
<ul>
<li><strong>评委打分一致性</strong>:部分项目评委意见分歧较小(如项目6),部分项目分歧较大(如项目2)</li>
<li><strong>分数集中现象</strong>:评委倾向于使用8分作为基准分</li>
<li><strong>极端评分</strong>:个别项目出现极端高分或低分</li>
<li><strong>解不唯一性</strong>:部分项目存在多个可能的评分分布</li>
</ul>
</div>
<div class="analysis-box">
<h3>建议改进</h3>
<ul>
<li>增加评分等级(如9分、7分等),减少分数跳跃</li>
<li>考虑引入小数点评分(如8.5分)</li>
<li>增加评委数量以减少统计偶然性</li>
<li>提供更详细的评分标准指导</li>
<li>考虑使用加权平均或其他统计方法处理异常值</li>
</ul>
</div>
</div>
<div class="footer">
<p>报告生成工具:Python数据分析脚本</p>
<p>注:本报告基于给定的总分数据,通过求解整数方程组获得可能的评分分布。</p>
<p>每个项目的"解数量"表示可能的评分分布组合数,值越大表示不确定性越高。</p>
</div>
<script>
// 简单的打印提示
document.addEventListener('DOMContentLoaded', function() {{
console.log('HTML报告加载完成。要打印报告,请使用浏览器的打印功能(Ctrl+P)。');
// 添加打印按钮(可选)
const printBtn = document.createElement('button');
printBtn.textContent = '打印报告';
printBtn.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
padding: 10px 20px;
background-color: #3498db;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
z-index: 1000;
`;
printBtn.onclick = function() {{
window.print();
}};
document.body.appendChild(printBtn);
}});
</script>
</body>
</html>
"""
# 保存HTML文件
with open(filename, 'w', encoding='utf-8') as f:
f.write(html_content)
print(f"\nHTML报告已生成: {filename}")
print("请在浏览器中打开该文件查看完整报告。")
print("提示:使用浏览器的打印功能(Ctrl+P)可以将报告保存为PDF。")
return filename
# 生成HTML报告
html_file = create_html_report(analysis_results, img1, img2, img3, img4)
# 输出控制台分析
print("\n" + "=" * 60)
print("深入分析:")
print("=" * 60)
# 计算各项目的平均分和标准差
print("\n各项目统计分析:")
for project, data in analysis_results.items():
score = data['score']
avg_score = score / 100
sol = data['best_solution']
# 计算标准差
scores_list = [10] * sol['10分'] + [8] * sol['8分'] + [6] * sol['6分']
mean = np.mean(scores_list)
std = np.std(scores_list)
print(f"\n{project} (总分: {score}):")
print(f" 平均分: {avg_score:.2f}")
print(f" 标准差: {std:.3f}(值越小表示评委意见越一致)")
print(f" 高分率(≥8分): {(sol['10分'] + sol['8分']) / 100:.1%}")
print(f" 满分率: {sol['10分'] / 100:.1%}")
print(f" 可能的评分分布组合数: {data['solution_count']}")
# 分析评委打分的一致性
print("\n" + "=" * 60)
print("评委打分一致性排名:")
print("=" * 60)
consistency_data = []
for project, data in analysis_results.items():
sol = data['best_solution']
scores_list = [10] * sol['10分'] + [8] * sol['8分'] + [6] * sol['6分']
std = np.std(scores_list)
consistency_data.append((project, std, data['score']))
# 按标准差排序(越小表示一致性越高)
consistency_data.sort(key=lambda x: x[1])
for i, (project, std, score) in enumerate(consistency_data, 1):
print(f" {i}. {project}: 标准差={std:.3f} (总分: {score})")
print("\n" + "=" * 60)
print("关键发现:")
print("=" * 60)
print("1. 项目6以950分获得冠军,项目3以930分获得亚军")
print("2. 高分项目(项目6、项目3)的评委意见更趋一致(标准差较小)")
print("3. 项目5得分最低(694分),且6分评委比例较高")
print("4. 每个项目都存在多个可能的评分分布(解不唯一性)")
print("5. 评委群体整体上更倾向于给出8分和10分")
HTML报告方案有以下优点:
-
完全避免字体问题:使用HTML和CSS,不依赖系统字体
-
交互性:可以在浏览器中直接查看,添加了打印按钮
-
美观性:使用CSS样式美化,包含响应式设计
-
完整性:包含所有分析内容、图表和数据表格
-
可打印性:可以直接在浏览器中打印成PDF
-
跨平台:在所有操作系统和浏览器上都能正常显示
生成的HTML报告包含:
-
所有分析图表(嵌入为base64图片)
-
详细的评分分布数据
-
奖项分配结果
-
误差分析和改进建议
-
打印功能
当然,如果大家想把这个生成PDF文件也很简单:
可以在浏览器中打开生成的HTML文件,然后使用浏览器的打印功能(Ctrl+P)将其保存为PDF格式。

点击打印:

此时会弹出如下的界面:

找到一个合适的路径,点击保存即可,最终会在你选定的地方生成一个PDF文件:

最终打开后就是这样的:
