news 2026/10/14 2:56:41

用Python脚本统计比赛最高分:从Pandas清洗到多轮扩展

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用Python脚本统计比赛最高分:从Pandas清洗到多轮扩展

2024-9-15那天,我在整理一场内部友谊赛的得分记录时发现,要从几十行杂乱的成绩数据里人工找出“最高分”,远比想象中麻烦——不是看一眼就能完事,而是要反复核对选手名称、场次编号和得分列,稍不留神就看串行,甚至把某个选手的登记分当成了最终成绩。也是那天我决定花点时间,把这个“统计比赛得分最大”的小需求做成一个可复用脚本。这篇博文就记录这个项目的完整过程:从需求定义、方案选型、代码实现到边界情况处理,再到后续多轮赛制的扩展改造。不管你是刚开始学Python、还是经常要手工处理成绩统计的赛事组织者,这篇内容都能给你一套可以照抄的解决方案。

1. 为什么“找最大得分”这件事,值得单独写一个脚本

1.1 数十行成绩记录里的人工查找,远没有想象中可靠

我们很多时候觉得“最高分不就是MAX函数一拉嘛”,但实际场景里的数据往往不是一张干净整齐的表格。我在2024-9-15处理的这份记录,来自一次持续多半天的交流赛,数据分散在几个不同的汇总表里:每张表有比赛轮次、选手编号、选手姓名、各裁判打分、最终得分。当时要回答一个问题——“今天所有场次里,谁的单场得分最高?”

如果用眼睛去看,你得先把十几张表格合并到一张,再确认“最终得分”这一列没有漏填、没有把“8.5”写成“8,5”,然后才能谈得上比较大小。

而一旦表格超过几十行,人工查找就会出现两个问题:一是容易漏看,扫过一列数字时对中间的峰值不敏感;二是容易看错行,特别是在多列数字结构一致的情况下,视线很容易从得分列滑到旁边的小分列。

这时写一个脚本的价值就体现出来了——它不光是帮你“求一个最大值”,更重要的是把整个统计流程固化成代码,从“读数据、清洗数据、计算、输出”每一步都可复现,下次再收集到新成绩时,只要数据格式不变,跑一遍脚本就能得到结果。

1.2 “最大得分”的定义,直接决定了代码怎么写

动手写代码之前,我第一件做的事情是确认“最大”到底指什么。虽然听起来有点小题大做,但不同赛制下“最大得分”这个指标差别很大:

  • 如果是单轮比赛,直接对得分列求最大值,拿到该轮得分最高的人。
  • 如果是多轮比赛,“最高分”可能指每位选手在多轮中的个人最高分,也可能指所有选手、所有轮次里的绝对最高分。
  • 还有一种常见口径:先求每位选手的总分,再在总分里找最大值。

这个定义不确认好,后面写的代码很可能在高高兴兴跑完脚本后,发现结果根本不是业务方要的那个数。

我当时的项目明确是“统计比赛得分最大”,结合来源数据,选手在每轮只有一个最终得分,所以核心口径就是:在给定数据范围内,找得分列的最大值,并同时输出该得分对应的选手和场次信息。确认这一点之后,再做方案选型就顺了。

2. 三种统计方案的取舍:Excel函数、SQL脚本、Pandas脚本

2.1 三个常用方案的优劣势对比

下表是我在实际考虑时整理的对比,直接列出来供参考。

方案上手难度处理混合格式数据可复用性扩展性适合场景
Excel 手工操作 + MAX/MATCH函数低弱,遇到文本型数字、乱码列名就很头疼低,每次都要重新改区域弱一次性、单表、数据量小
SQL 查询语句中中,对CSV文件要先做导入建表较高,SQL可保存中数据已经在数据库里
Python + Pandas 脚本中强,可写代码处理各种脏数据高,改改路径就能复用强多文件、多轮次、字段不规整

对于Excel,优点是“所见即所得”,但有几个场景直接劝退:当我需要把多张CSV合并后再统计,Excel需要手工复制粘贴;当得分列被识别为文本时,MAX函数直接返回0或者无效结果,排查起来又得去检查单元格格式。对于SQL,如果数据已经在数据库中,一条“SELECT MAX(score)”就结束了,可问题是我们手上的初始数据就是几个Excel文件,为了这个临时需求去建表导入,反而绕远了。

2.2 我为什么最终选了Pandas

Pandas的优势不只是“会统计”,而是把前面最痛苦的“脏数据规整”这个环节也收纳进来了。它可以直接读取CSV、Excel,可以把“8,5”这种带逗号的文本修正为浮点数,可以用dropna过滤空值,也可以按照轮次和选手分组聚合。后面一旦要扩展统计口径,比如多轮赛制下的最高分、每轮前三名、某个选手的最好成绩,基本上都是在原脚本上增加几行代码。

从另一个角度说,Python脚本运行一次后,可以用“文件+日期”的方式保存脚本和数据,比如项目标题里的“2024-9-15”,就是当天专门建的项目记录目录名。这样以后有人问“这个最高分是从哪些数据算出来的时候”,我可以直接把原始文件和运行脚本都拿给他,保证统计结果可追溯。这比基于Excel手工操作的结果要严谨得多。

如果你只是需要一次快速查询,那完全可以停留在Excel,不必为了一个最大值去装Python环境。但如果你的需求是“以后每次比赛结束都想复用同一个脚本”,那我建议直接用Pandas,把一次性处理升级成一个小工具。

3. 核心脚本实现:读取成绩表、清洗字段、输出最高得分

3.1 第一步:约定输入格式并读取数据

在写任何代码之前,我先把源数据整理成了统一的格式。因为原始比赛记录有几个散表,我合并成了一张完整的CSV文件,列结构如下:

字段名说明示例
round_no轮次编号R1
player_id选手编号P001
player_name选手姓名某选手A
raw_score各裁判小分,用逗号分隔8.5, 9.0, 8.0
final_score最终得分8.75

之所以额外保留raw_score,是为了在统计最高分时还能追溯“这个最终得分是怎么来的”。final_score才是我们真正要参与最大值计算的字段。

读取数据用Pandas就两行:

import pandas as pd df = pd.read_csv("scores_20240915.csv", encoding="utf-8") print(df.head())

这里有一个细节:读取CSV时如果直接显示乱码,多半是文件编码不是UTF-8,而是GBK等常见中文编码。我当时第一次读取后就踩了这个坑,后来改用参数动态处理:

def load_scores(path): try: df = pd.read_csv(path, encoding="utf-8") except UnicodeDecodeError: df = pd.read_csv(path, encoding="gbk") return df

先把编码问题解决掉,后面所有字段的规整才有讨论意义。

3.2 第二步:预处理得分列,解决类型和空值问题

读取完成后,不要急着求max。我先检查了final_score列的实际类型:

print(df["final_score"].dtype)

如果dtype是object,说明这一列在CSV里是文本格式,直接求max会得到按字典序排序的结果,这是新手最容易掉进的坑。比如“9.5”和“10”在文本比较时,会被判断为“9.5”更大,因为字符串比较按字符逐个比,“9”确实比“1”大,但作为数值显然10比9.5更高。

所以要把这一列强制转成数值类型:

df["final_score"] = pd.to_numeric(df["final_score"], errors="coerce")

带上errors="coerce"的意思是:遇到无法转换的内容时,不直接报错中断,而是把那个位置填成NaN,这样后续还能知道哪些行数据有问题,再做针对性处理。处理完以后,查看空值行:

bad_rows = df[df["final_score"].isna()] print(bad_rows)

这一步的产出价值是很大的——我在整理数据时确实发现两行得分填写成了“8,75”(英文逗号),转成numeric后变NaN。如果你的目标是统计最大得分,这类数据不处理的话,要么直接报错,要么被静默跳过,结果可信度都打折扣。

3.3 第三步:用正确的方式求最大值并定位所在行

Pandas里求最大值有两种用法,很多人会混淆。第一种是直接用max(),拿到最高分这个数字本身:

max_score = df["final_score"].max() print(max_score)

第二种更常用,因为我们不仅仅要一个数字,还要知道“谁得了这个最高分”。这时要改用idxmax(),它会返回最高分所在行的行标签:

idx = df["final_score"].idxmax() best_row = df.loc[idx] print(best_row)

为什么选idxmax而不是先排序再取第一行?因为sort_values会生成一个完整的新副本,数据量小的时候无所谓,但假设有几千上万条成绩记录,idxmax只需线性扫描一遍找到最大值的位置,效率更高,代码语义也更直接。

拿到best_row之后,我格式化成方便人读的文本:

output = ( f"最高得分:{best_row['final_score']}\n" f"对应选手:{best_row['player_name']}(编号{best_row['player_id']})\n" f"轮次:{best_row['round_no']}" ) print(output)

核心统计逻辑到这一步其实已经完成了,运行一次可以得到类似:

最高得分:9.80 对应选手:某选手E(编号P012) 轮次:R4

这种输出适合直接粘贴到汇报文档里,一眼就能看到关键信息。

3.4 完整脚本怎么看:从数据源头到结果的映射关系

把前面几步连起来,就是一个最小可用的版本:

import pandas as pd def load_scores(path): ... def preprocess(df): df["final_score"] = pd.to_numeric(df["final_score"], errors="coerce") return df def find_max_score(df): cleaned = preprocess(df) idx = cleaned["final_score"].idxmax() return cleaned.loc[idx] if __name__ == "__main__": df = load_scores("scores_20240915.csv") best = find_max_score(df) print(best)

整个脚本的映射关系很清晰:源数据CSV -> DataFrame -> 清洗后的得分列 -> idxmax定位 -> 完整结果行。即使你之后要改统计口径,要改的也只是一处函数,而不是在Excel里重新拖一遍公式。

4. 实际跑数据时反复遇见的边界情况

真实的数据永远比示例数据难看,以下四类边界情况是我在2024-9-15这个项目里反复处理过的,不处理好它们,“统计最大得分”的结果就会被质疑。

4.1 得分列是文本数字,max()直接给出错误结果

前面提到过,文本型数字在max()时按字典序比较。这个问题的隐蔽性在于:如果得分都在8分到9分之间,没有超过10的分数,文本比较还真能碰巧得出正确结果;一旦出现10分、10.5这种两位数的数值,结果立刻出错。最稳妥的防御性做法是读取后先统一执行pd.to_numeric,而不是等到发现结果不对再回头检查。

我在清洗过程中还遇到另一个衍生的麻烦:某个单元格的得分带有不可见字符,比如“8.5 ”后面跟了一个空格,或者全角数字“8.5”。pd.to_numeric对空格其实能自动处理,但全角数字会转换失败。这时候需要先做一次字符替换:

df["final_score"] = ( df["final_score"] .astype(str) .str.replace(",", ",") .str.extract(r"(\d+\.?\d*)", expand=False) ) df["final_score"] = pd.to_numeric(df["final_score"], errors="coerce")

这段代码先用正则从混乱文本中提取第一个合理的数字表达式,再转换为数值。适用于那种“某人手工填表,把逗号、中文逗号混着用”的典型情况。

4.2 空值和0分的区别,不能默认等同

在比赛场次中,选手可能因为缺席而没有成绩,也可能正常参赛但得分为0。这两种情况在统计最高分时都需要区别对待:缺席的空值参与max()会被Pandas默认跳过,不干扰结果;但0分是有效成绩,必须保留参与比较。

问题在于,很多人在Excel阶段就会把0分和空缺混在同一个单元格,比如用“-”或者“”表示没参赛。在Pandas里读取时,“-”会被整列转成object类型。所以我在预处理时明确做了两步:先把“-”、“NA”、“空文本”统一替换成NaN,表示无成绩;再把剩下的数值部分转为数值类型。

missing_markers = ["-", "NA", "N/A", ""] df.replace(missing_markers, pd.NA, inplace=True) df["final_score"] = pd.to_numeric(df["final_score"], errors="coerce")

这样的好处是后续统计的时候可以自由选择口径:如果想要“参赛范围内的最高分”,直接max();如果要把缺席视为“该选手此轮得0分”,先fillna(0)再max()。两种口径,一行代码切换。

4.3 并列最高分时,只输出一个结果是信息丢失

idxmax()遇到多个并列最大值时,只会返回第一个出现的位置。从业务角度看,这可能不是一个Bug,但确实存在信息丢失。比如一轮比赛中有两位选手同时拿到9.8分,谁才是“最高分获得者”?如果没有并列处理,你可能只看到其中一位的名字。

所以我在项目里专门加了一个并列检查逻辑:

max_score = df["final_score"].max() tied = df[df["final_score"] == max_score] if len(tied) > 1: print(f"并列最高分 {max_score} 共{len(tied)}位选手:") for _, row in tied.iterrows(): print(f" - {row['player_name']}({row['player_id']},轮次{row['round_no']})") else: row = tied.iloc[0] print(f"最高得分:{row['final_score']},对应选手:{row['player_name']}")

这个检查的价值在于:它把一个隐性问题显式化。比赛主办方拿到统计结果时,最怕的就是漏掉并列人员。脚本主动检测并列并分别列出来,后面就不会出现“谁最高”的争议。

4.4 多张分表合并时的脏数据累积

初始数据并非一张表,而是按轮次分开的多个文件。如果每个文件都有不同的字段顺序或列名写法(比如有的叫“选手姓名”,有的叫“姓名”),直接concat会产生多个重复列。我当时的做法是先统一列名映射,再合并:

column_map = {"选手姓名": "player_name", "姓名": "player_name", "选手编号": "player_id"} for col, target in column_map.items(): if col in df.columns: df.rename(columns={col: target}, inplace=True)

合并完先检查总行数,再检查是否有缺失的姓名字段。这一步在统计前多做五分钟,后面调用统计脚本时就能少很多莫名其妙的问题。

5. 从“单次统计”升级到“多轮赛制”的扩展改造

5.1 按选手分组,统计各人的历史最高分

第一版脚本跑通之后,我开始想:如果这个比赛要连续进行多轮,那“统计比赛得分最大”的需求就不会只问一次,而是会衍生出更多问题。最常见的是“每位选手在多轮比赛中的个人最高分是多少”。

实现上用groupby就可以:

personal_best = ( df.groupby("player_id", as_index=False) .agg(max_score=("final_score", "max"), best_round=("round_no", "max")) ) print(personal_best.sort_values("max_score", ascending=False))

这里用max去聚合round_no其实有个隐含假设:如果某选手在同一轮次只存在一条记录,那么取到的轮次和最大分来自同一行。如果原始数据中存在同一轮同一选手多条记录,这个写法就可能把最高分和轮次错配。更严谨的做法是先按轮次求本轮最大分,再按选手求跨轮最大值:

round_best = ( df.groupby(["player_id", "round_no"])["final_score"] .max() .reset_index() ) personal_best = ( round_best.sort_values("final_score", ascending=False) .drop_duplicates(subset=["player_id"]) .sort_values("final_score", ascending=False) )

这才是经得起追问的统计链路。

5.2 按指定轮次筛选,输出该轮的最高分及对应选手

另一种频繁出现的统计口径是:“只看某一轮,这一轮谁最高”。这个场景适合把轮次参数化,免得每轮都改代码:

def max_of_round(df, target_round): sub = df[df["round_no"] == target_round].copy() idx = sub["final_score"].idxmax() row = sub.loc[idx] print(f"轮次{target_round}最高分:{row['final_score']}")

这样以后每一轮结束,把新的得分表追加到总表后,循环处理所有轮次即可:

for r in df["round_no"].unique(): max_of_round(df, r)

5.3 用自动化校验让统计结果可追溯

脚本写多了之后,我给自己加了一条规则:所有统计输出必须能被验证。最简单的校验方法是用两种互相独立的方式求最大值,然后对比结果是否一致。一种方式是max(),另一种方式是sort_values后取第一条:

max_method1 = df["final_score"].max() max_method2 = df["final_score"].sort_values(ascending=False).iloc[0] assert max_method1 == max_method2

以及用计数校验,检查输出结果对应的选手行数在源数据里确实存在:

assert len(df[df["player_id"] == best_id]) >= 1

这些assert本质上不是为了防Pandas出错,而是防“数据被意外改动”导致统计结果和业务对不上。赛后有人质疑分数时,我能直接说:这个结果是从原始数据哪个字段算出来的、经过了几道清洗,每一步都有代码记录。

把脚本和CSV文件放在同一天建好的目录里,目录名就带当天日期,比如2024-9-15。这个习惯帮你省掉了“这结果到底是哪一版数据算出来的”这类重复沟通,时间久了会发现特别值得坚持。


回看这个项目,核心逻辑其实很简单——读取数据、洗数据、求最大值、输出结果——但真正让脚本有价值的,是那些提前想清楚的统计口径、边界情况的处理办法,以及可追溯的检查机制。2024-9-15之后,这个小脚本我又在后续几场比赛中反复跑了很多次,每次只需要把新的CSV文件放进去,改一下路径就能用。如果你也经常和比赛得分表打交道,建议按这个思路把它升级成自己顺手的小工具,省下的时间足够你再去做更多有意义的事情。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 2:56:35

CUPT尺子魔术:三个时间尺度与大气压锁紧的定量实验方案

简介:2024年CUPT尺子魔术(Ruler Trick)市一等奖方案PPT,是面向物理学术竞赛参赛团队与指导教师的完整实验方案,用于讲清白纸覆盖后尺子为何不掉落、球被弹起的力学机制及关键参数影响。内容按题目解读、预实验、理论模…

作者头像 李华
网站建设 2026/10/14 2:54:12

NLP变长序列处理:PyTorch/TensorFlow/JAX实战避坑指南

1. 为什么“变长序列”是NLP模型落地的第一道坎在某跨平台文本分析系统上线前的压测阶段,我们发现一个看似简单的意图识别模块,在处理用户输入的短句(如“查余额”)和长段落(如客服工单中300字的故障描述)时…

作者头像 李华
网站建设 2026/10/14 2:53:54

Flink入门实战:从核心概念到第一个实时程序

1. 为什么我要花时间写这篇 Flink 入门拆解第一次接触 Flink 是在一个实时报表项目里。当时业务方要求“数据产生后 3 秒内必须出现在大屏上”,团队一开始用定时任务每 5 分钟跑一批,结果被业务方追着问“为什么刚才那笔订单没显示”。后来换成 Flink&am…

作者头像 李华
网站建设 2026/10/14 2:53:33

英语教学5个常见误区,2026年最新整理,越早避开越好

【摘要】英语教学里有些误区,踩进去的老师不在少数。本文从行业观察和技术实践的角度,拆解5个高频误区,聊聊天学网在英语教学体系化上的思路,以及实际教学场景里怎么避开这些坑。带过几届学生之后回头看,有些问题真是年…

作者头像 李华
网站建设 2026/10/14 2:52:55

Java实现华容道自动求解:BFS与A*搜索算法实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华