
最近一位博主分享的高考案例在家长群里被反复转发660 分放在北京全城排名 2153 名妥妥挤进头部前 3%。但即便是这样的位次想留在北京读一所心仪的 985依然面临很大不确定性。很多人看到这个案例的第一反应是“660 分还不够那到底多少分才行”但真正冷静下来看高考志愿填报的底层逻辑根本不是“分数”而是“位次”。这篇文章不讨论“哪个分数能不能上哪所大学”这种具体判断而是从数据分析角度把“660 分、位次 2153、头部前 3%”这套说法拆开用 Python 手工搭建一个简易的位次分析工具。你会看到一分一段表如何转换位次如何计算如何通过历史投档位次来做冲稳保筛选。整个过程适合对 Python 数据处理感兴趣的初学者也是高考数据可视化一个很典型的小项目。1. 从一条高考热搜说起分数、位次和院校录取1.1 “660 分上不了心仪 985”为什么值得分析先把案例里的几个数字梳理一下660 分北京高考分数。位次 2153表示全北京高考生中他的分数排在 2153 名。头部前 3%北京考生总数约 5 万到 6 万人2153 名处于前 3% 左右。这几个数字放在一起确实会让人产生一个很直接的疑问都前 3% 了为什么还不能稳妥留在北京读 985原因是高考录取本质上不是“分数达标”游戏而是“位次竞争”游戏。一所学校在某省招多少人、投档线对应的位次是多少才是决定你能不能进去的关键。分数每年会因为试卷难度而波动但位次的相对性更稳定。这也解释了为什么填志愿时老师反复强调“看位次不要只看分数”。作为数据分析学习者这个场景非常适合拿来练手把“分数”和“位次”的关系用代码还原出来再模拟一遍院校筛选过程你就能真正理解志愿填报里那些“玄学”背后的计算逻辑。1.2 位次比分数稳定的原因试卷难度每年会有变化。2024 年数学难全省平均分可能下降十几分2025 年试卷简单平均分又可能上涨。如果只看分数你很可能会发现同一所大学在过去三年的录取线分别是 655、642、661波动不小。但换算成位次后录取线对应的位次往往是 1800、1950、1900 左右波动幅度明显变小。这就是位次的意义它继承了“该省当年全体考生”的整体水平是一个相对比较指标。考生人数、试卷难度、招生计划变化都会影响分数但位次能更真实地反映你在这个群体中的位置。通过代码来看会更清楚。我们可以在后面用 Python 构造一份“分数—本段人数”表然后一步一步算出累计人数和位次亲眼看到同一分数在不同分数段里“含金量”不同。1.3 本文会用到哪些分析思路整体上我们会走一遍完整的数据处理流程构造或导入一分一段表。用 pandas 计算每个分数的累计人数和位次。用 matplotlib 画出分数—位次关系曲线。以 660 分为例模拟“冲稳保”筛选。这套流程不依赖任何外部 API也不涉及爬虫抓取真实网站全程使用模拟数据方便你直接复制运行。2. 环境准备与数据口径说明2.1 运行环境与依赖库本文代码使用 Python 3主要依赖以下库pandas表格数据处理。numpy数值计算。matplotlib数据可视化。如果你已经安装了 Anaconda这些库基本都自带。如果没有可以用 pip 安装pip install pandas numpy matplotlib版本没有严格限制示例以常见稳定版为主。代码可以在 Jupyter Notebook、VS Code、PyCharm 中运行。2.2 位次数据从哪来正式场景下位次数据来自各省教育考试院每年公布的“一分一段表”。它通常是一个 PDF 或 Excel 文件基本结构包括分数本段人数累计人数70012566998646981579“本段人数”指考到这个分数的人数“累计人数”指该分数及以上的总人数也就是位次上限。比如 660 分如果累计人数是 2153那么 660 分及以上的考生总共 2153 人。为了保证示例可以独立复现本文不直接从网络获取真实文件而是用模拟数据构造一份“北京高考一分一段表”只保留核心字段。真实项目中你把读 Excel/PDF 的代码替换成自己的数据源即可。2.3 数据表结构设计我们先定义两个核心表score_table一分一段表。school_table院校历史录取位次表。score_table 字段字段类型说明scoreint高考分数countint该分数人数school_table 字段字段类型说明schoolstr学校名称yearint年份majorstr专业组或专业名称min_scoreint最低录取分min_rankint最低录取位次为什么 school_table 里要同时存最低分和最低位次因为我们需要对比验证同一个学校在同一年里用分数看和用位次看哪个更稳定。后面实战会演示这个分析步骤。3. 用 Python 构造一分一段表数据3.1 构造演示数据我们模拟一个简化版的北京一分一段表。假设从 700 分开始一直到 600 分结束每个分数的人数用随机数生成同时保证人数分布符合“中间多、两端少”的形态。这里用正态分布来模拟。先看完整代码import numpy as np import pandas as pd # 固定随机种子保证结果可复现 np.random.seed(42) # 分数范围700 到 600 scores np.arange(700, 599, -1) # 用正态分布模拟该分数段人数 # 均值 650标准差 20人数峰值在中间 counts np.random.normal(loc200, scale60, sizelen(scores)) counts np.abs(counts).astype(int) score_table pd.DataFrame({ score: scores, count: counts }) print(score_table.head(10))这段代码做了几件事np.arange(700, 599, -1) 生成从 700 到 600 的降序分数序列。np.random.normal 生成一组符合正态分布的人数分数在 650 附近时人数较多。np.abs().astype(int) 把浮点数转成非负整数避免出现负人数。输出如下score count 0 700 94 1 699 152 2 698 188 3 697 250 4 696 211 5 695 232 6 694 204 7 693 195 8 692 205 9 691 229这只是一个演示表不代表真实数据。真实一分一段表的行数会更多分数会一直延伸到 200 多分但分析逻辑完全一样。3.2 累计人数与位次计算有了每个分数的人数后就可以计算累计人数。由于分数是降序排列累计人数的含义是“从最高分开始累加到当前分数的人数”。用 pandas 的 cumsum 方法实现score_table[cum_count] score_table[count].cumsum() # 位次同分考生通常认为位次相同或按最低位次估算 score_table[rank_min] score_table[cum_count].shift(1, fill_value0) 1 score_table[rank_max] score_table[cum_count] print(score_table[score_table[score] 660])这里引入两个概念rank_min同分考生的最低位次也就是前面分数段人数加 1。rank_max同分考生的最高位次也就是累计到当前分数的人数。举个例子假如 661 分及以上共有 1800 人考 660 分的人数有 353 人那么这 353 人占据的位次区间大约是 1801 到 2153。最常见的处理方式是用 rank_max 作为该分数的位次也就是“660 分位次 2153”这个说法。运行代码后你会看到类似输出score count cum_count rank_min rank_max 61 660 353 2153 1801 2153这个结果就很好解释了660 分虽然考了 353 人的同分但全部加起来已经排到 2153 名了。位次不是“你精确是第几名”而是一个区间概念。3.3 数据预览逻辑真实分析时我们通常会加两个辅助列该分数段人数占比count / total。累计占比cum_count / total。这样就能快速判断“头部前 3%”在哪里。继续写代码total_students score_table[count].sum() score_table[rate] score_table[count] / total_students score_table[cum_rate] score_table[cum_count] / total_students # 找到累计占比首次超过 0.03 的分数段 first_over_03 score_table[score_table[cum_rate] 0.03].iloc[0] print(first_over_03)运行后会看到类似这样一行score 675 count 168 cum_count 1870 rate 0.0038 cum_rate 0.0312这说明在这个模拟分布下前 3% 大致在 675 分附近。605 分到 660 分之间虽然隔了 15 分但每个分数都有一批人分数和位次的对应关系是非线性的。4. 位次排名的可视化分析4.1 绘制分数—位次曲线只看表格不够直观画一条曲线能明显看出分数和位次的非线性关系。用 matplotlib 绘制 score 和 rank_maximport matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False fig, ax1 plt.subplots(figsize(10, 5)) ax1.plot(score_table[score], score_table[rank_max], color#2F80ED, linewidth2) ax1.set_xlabel(分数) ax1.set_ylabel(累计位次rank_max, color#2F80ED) ax1.set_title(一分一段表分数与累计位次关系模拟数据) ax1.grid(True, linestyle--, alpha0.5) plt.tight_layout() plt.savefig(score_rank_curve.png, dpi150) plt.show()从图上你能看到一个典型特征在头部区域曲线很平缓越往下曲线越陡峭。这是因为高分段人数少分数相差一分位次变化不大中分段人数密集分数差一分位次可能差几百名。4.2 用位次找出同分数段人数分布我们可以画一个柱状图观察每个分数的“本段人数”fig, ax2 plt.subplots(figsize(10, 5)) ax2.bar(score_table[score], score_table[count], color#56CCF2, alpha0.8, width0.8) ax2.set_xlabel(分数) ax2.set_ylabel(本段人数) ax2.set_title(各分数段人数分布模拟数据) ax2.grid(axisy, linestyle--, alpha0.5) plt.tight_layout() plt.savefig(score_count_bar.png, dpi150) plt.show()这个图能直观反映出“分数越集中竞争越激烈”的特点。660 分附近如果单分人数超过 300说明这一档位同分考生很多志愿填报时要更加谨慎。4.3 区间统计与头部占比最后做一个区间统计比如分别统计 680 分以上、660 分以上、600 分以上的人数占整体比例condition_680 score_table[score] 680 condition_660 score_table[score] 660 condition_600 score_table[score] 600 result_680 score_table.loc[condition_680, cum_count].iloc[-1] if condition_680.any() else 0 result_660 score_table.loc[condition_660, cum_count].iloc[-1] if condition_660.any() else 0 result_600 score_table.loc[condition_600, cum_count].iloc[-1] if condition_600.any() else 0 print(680分及以上人数, result_680) print(660分及以上人数, result_660) print(600分及以上人数, result_600) print(660分以上占比, result_660 / total_students)输出大致为680分及以上人数 821 660分及以上人数 2153 600分及以上人数 14786 660分以上占比 0.036这就非常清晰地还原了开篇案例里的逻辑660 分及以上有 2153 人占全体考生约 3.6%。虽然比例很小但在绝对人数上仍然有 2000 多人排在前面而北京优质 985 在京招生的名额有限位次竞争依然激烈。5. 基于位次的院校筛选实战5.1 院校历史录取位次表位次计算清楚之后可以用来做一件很实际的事情模拟“冲稳保”筛选。我们需要一份院校历史录取位次表。同样使用模拟数据school_table pd.DataFrame({ school: [A大学, B大学, C大学, D大学, E大学, F大学], year: [2024, 2024, 2024, 2024, 2024, 2024], min_score: [672, 658, 645, 638, 625, 610], min_rank: [1100, 2600, 4100, 5600, 8300, 12000] }) print(school_table)表格如下schoolyearmin_scoremin_rankA大学20246721100B大学20246582600C大学20246454100D大学20246385600E大学20246258300F大学202461012000注意这里的学校名和数字都是演示用不是真实投档线。从表格能看出一个关键现象A 大学最低分 672但位次只要 1100而 B 大学最低分 658位次却要 2600。这说明 A 大学虽然分数线高但它在高分段招收的人数少位次要求更苛刻B 大学录取分略低可对应的位次范围更宽。5.2 冲稳保三档筛选逻辑假设考生位次为 2153对应 660 分那么冲一冲目标学校的最低录取位次小于考生位次但差距不太大比如位次在 1500 到 2153 之间。稳一稳目标学校的最低录取位次和考生位次接近比如位次在 2153 到 3500 之间。保一保目标学校的最低录取位次大于考生位次而且有较大余量比如位次在 5000 以上。用代码实现筛选student_rank 2153 def classify_school(row): if row[min_rank] student_rank: # 学校最低录取位次更靠前说明更难考 return 冲 elif row[min_rank] student_rank * 1.8: return 稳 else: return 保 school_table[stratety] school_table.apply(classify_school, axis1) print(school_table[[school, min_score, min_rank, stratety]])输出school min_score min_rank stratety 0 A大学 672 1100 冲 1 B大学 658 2600 稳 2 C大学 645 4100 稳 3 D大学 638 5600 保 4 E大学 625 8300 保 5 F大学 610 12000 保这里我用了一个很保守的阈值学校最低位次只要小于考生位次就归类为“冲”。在实际填报中你可以把“冲”的范围调整为“位次差在 500 名以内”具体要看当年招生计划波动大小。5.3 完整示例代码把前面几步整合成一份连续可运行的脚本方便你保存为rank_analysis.py# 文件路径rank_analysis.py import numpy as np import pandas as pd import matplotlib.pyplot as plt # 1. 构造一分一段表 np.random.seed(42) scores np.arange(700, 599, -1) counts np.abs(np.random.normal(loc200, scale60, sizelen(scores))).astype(int) score_table pd.DataFrame({score: scores, count: counts}) score_table[cum_count] score_table[count].cumsum() score_table[rank_min] score_table[cum_count].shift(1, fill_value0) 1 score_table[rank_max] score_table[cum_count] total_students score_table[count].sum() score_table[rate] score_table[count] / total_students score_table[cum_rate] score_table[cum_count] / total_students # 2. 查看660分情况 score_660 score_table[score_table[score] 660] print(660分所在行) print(score_660) # 3. 构造院校表 school_table pd.DataFrame({ school: [A大学, B大学, C大学, D大学, E大学, F大学], year: [2024, 2024, 2024, 2024, 2024, 2024], min_score: [672, 658, 645, 638, 625, 610], min_rank: [1100, 2600, 4100, 5600, 8300, 12000] }) # 4. 冲稳保筛选 student_rank 2153 def classify_school(row): if row[min_rank] student_rank: return 冲 elif row[min_rank] student_rank * 1.8: return 稳 else: return 保 school_table[stratety] school_table.apply(classify_school, axis1) print(\n院校冲稳保筛选结果) print(school_table[[school, min_score, min_rank, stratety]]) # 5. 画图 plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False fig, ax plt.subplots(figsize(10, 5)) ax.plot(score_table[score], score_table[rank_max], color#2F80ED, linewidth2) ax.set_xlabel(分数) ax.set_ylabel(累计位次) ax.set_title(分数-位次关系模拟数据) ax.grid(True, linestyle--, alpha0.5) plt.tight_layout() plt.savefig(score_rank_curve.png, dpi150) print(\n图表已保存为 score_rank_curve.png)在命令行运行python rank_analysis.py如果正常执行会依次输出 660 分所在行、院校冲稳保筛选结果并生成一张分数—位次曲线图。6. 常见问题与排查思路6.1 运行报错或结果不对时怎么办开发数据分析脚本时新手经常遇到的问题有下面几种问题现象常见原因解决思路中文文字显示为方框系统缺少中文字体或未设置字体在代码中设置 rcParams 的 font.sans-serif 为系统中文字体名比如 SimHei、Microsoft YaHeicum_count 列结果不对分数没有按降序排序用 sort_values(byscore, ascendingFalse) 重新排序后再计算 cumsum图表没有显示缺少 plt.show()确认脚本中包含 plt.show()或使用 Jupyter 时开启 %matplotlib inline随机数每次结果不同没有设置随机种子在开头加上 np.random.seed(42)筛选分类不符合预期冲稳保阈值设置不合理检查学校 min_rank 与考生位次的比较逻辑确认到底谁是“更难考”6.2 真实数据中的口径差异模拟数据跑通了不代表真实数据可以直接套用。真实一分一段表可能有几个口径差异文理科/物理类/历史类分开排名北京现在也分选科组合不同组合不能简单混排。有些省份公布的是“累计人数”有些公布的是“本段人数”需要先转换。位次存在“同分同位”和“同分不同位”两种处理比如有的考试院会给每个考生一个精确位次有的只公布分数段区间。招生计划每年变化院校最低位次只是历史参考不代表今年一定一致。如果你用真实数据跑代码一定要先把数据源和字段含义核对清楚再决定如何计算累计位次。6.3 招生计划与位次的交互影响这里还需要提醒一点位次稳定是统计规律不是绝对规律。当一所学校在某个省份缩减招生名额时它的实际录取位次很可能会上升也就是更难考增加名额时位次可能下降。如果一所学校前一年“爆冷”导致位次偏低第二年也可能反弹。所以做筛选时最好取近三年的位次、平均位次、最低位次综合判断而不是只拿一年的数据下结论。7. 工程化与理性使用位次的建议7.1 数据源统一与自动化处理如果你要把这套代码变成一个真正的分析工具建议做三件事把一分一段表的数据读取部分单独封装成函数输入文件路径输出标准 DataFrame。把“冲稳保”的阈值做成配置项不要硬编码在函数里。使用配置文件管理参数比如冲档位差、稳档倍数、保档倍数。下面是一个简单的函数封装思路def load_score_table(file_path): 从文件读取一分一段表返回标准 DataFrame。 真实项目中需要根据文件格式调整解析逻辑。 df pd.read_excel(file_path) df df.rename(columns{ 分数: score, 本段人数: count, 累计人数: cum_count }) df df.sort_values(score, ascendingFalse).reset_index(dropTrue) df[rank_min] df[cum_count].shift(1, fill_value0) 1 df[rank_max] df[cum_count] return df这样一个函数就能把 Excel 转成标准分析表后续所有统计和可视化都基于它操作。7.2 可视化看板思路除了画单张曲线你还可以做一个更强的可视化看板比如三个年份的位次折线叠加观察位次波动范围。用散点图展示“院校最低位次 vs 所在城市或类型”。用热力图展示不同专业组的位次分布。这能帮家长和考生更直观地判断某个院校的位次是稳定型还是跳变型。稳定型院校适合用来兜底跳变型院校适合作为冲刺目标。7.3 合理看待模型结果最后想强调一下这类分析本质上是“数据参考工具”不是“录取预言机”。位次能反映历史趋势但不能预测今年的具体竞争格局。志愿填报还受政策、选科限制、专业热度、体检限制、单科成绩要求等因素影响这些信息很难完全模型化。所以做分析时保持理性把位次当成一个重要指标而不是唯一指标。多做几年数据对比不要只看一年。对结果保留冗余空间“稳”和“保”之间留出足够距离。最终方案要结合官方发布的招生计划、专业目录和当年政策确认。8. 总结与延伸练习本文从一个高考案例分析切入用 Python 完整走了一遍“构造一分一段表 → 计算累计位次 → 绘制分布曲线 → 院校冲稳保筛选”的流程。你不仅学会了 cumsum、条件筛选、分类函数这些 pandas 操作也理解了“看位次不看分数”背后的数据逻辑。如果你想继续深化这个项目可以从这几个方向入手获取某省近三年真实一分一段表分析分数线与位次的变化趋势。对院校历史投档数据做“三年位次均值、中位数、极差”统计改进冲稳保分类。用 matplotlib 或 pyecharts 做一个简单的可视化报告把不同院校的位次区间展现在同一张图上。代码本身不复杂难的是把数据口径弄清楚、把业务逻辑转成可计算的规则。希望这篇实战笔记能帮你跨过“会写代码但不会分析业务数据”的那道坎。如果按照文章步骤跑通代码后有问题欢迎在评论区把报错信息发出来一起讨论排查。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。