个人技术分享

技术笔记

从零实现一个 Elo 评级回放器

· 标签:算法 · 数据建模

要做战队实力排名,最简单的做法是统计胜率。但胜率有个致命问题:它不区分对手强度。赢了强队和赢了弱队记一样的分,赛程轻松的队伍排名会虚高。Elo 的核心价值就是把这个区分补上。

公式本身很短

Elo 只有两步。第一步算期望胜率,第二步按实际结果与期望的差值修正:

// 期望胜率:A 战胜 B 的概率
E_a = 1 / (1 + Math.pow(10, (R_b - R_a) / 400))

// 更新:S 为实际得分(胜 1 / 平 0.5 / 负 0)
R_a' = R_a + K * (S - E_a)
R_b' = R_b + K * ((1 - S) - E_b)

400 这个常数决定曲线的陡峭程度,差值 400 分对应约 10:1 的胜率比。两个人的分数变动互为镜像,总分守恒,这一点在实现时可以用来做自检。

真正麻烦的是这四个决定

1. 初始分怎么给

所有人给同一个值(常用 1500)会导致前几场分数剧烈波动,因为期望值一直是 0.5。如果用"逐场重放"的方式,冷启动的噪声会随着场次增加自然衰减;但如果只跑少量比赛,就得考虑按赛区基线给不同初始分。我倾向于让不同赛区的初始分有差异,否则跨赛区比赛的第一次交锋会给出很离谱的修正。

2. K 值取多少

K 决定单场结果能撬动多少分。K 大,排名反应快但抖动大;K 小,稳定但迟钝。没有一个通用正确答案,它本质上是在"快速反映现状"和"抵抗偶然性"之间取舍。比较实用的做法是让 K 随场次递减:新队伍变动快一点,打了几十场之后逐渐稳定。

3. 系列赛和单图要分开算

这是最容易被忽略的一点。一场 BO3 里打了三张图,如果你既按每张图更新一次、又按整个系列赛再更新一次,同一份信息就被计了两次。我的做法是维护两套分数:系列赛分数用较大的 K(比如 40),单图分数用较小的 K(比如 8)。单图分数只用来做地图层面的强弱分析,不参与跨队排名。

4. 别用未来数据回放过去

最容易犯的错是把所有比赛按时间排序后一次性跑完,然后拿某场比赛之前的分数去解释那场比赛——但如果预处理阶段用到了后面的数据(比如全局平均分、整赛季的统计数据),就构成了数据泄漏。严格的做法是:任何用于某场比赛的输入,只能来自这场比赛开始之前已知的信息。

把过程存下来,而不是只存结果

只保存最终的分数榜,你无法解释"为什么这支队伍涨了这么多"。我让回放器每场都输出一条记录:

{
  matchId: "754730",
  teamA:  { before: 1620, delta: +18, after: 1638 },
  teamB:  { before: 1710, delta: -18, after: 1692 }
}

有了逐场明细,前端可以直接展示"这场赢了之后涨了几分",也能快速定位异常——比如某支队伍因为一次冷启动修正而虚高,一眼就能看出来。

最后做一次守恒自检

回放完成后,把所有队伍的 delta 加起来,应该严格等于 0(容差取浮点误差范围)。这个断言几乎不花成本,但能挡住绝大多数实现错误,比如漏更新了某一侧、或者平局处理时两边都按胜方算。

← 返回首页