如何在Pandas DataFrame中实现N玩家Elo评分算法?
多玩家Elo评分计算实现(Pandas)
问题描述
我有一个记录玩家比赛结果的Pandas DataFrame,包含以下字段:
Race_ID:赛事唯一IDRacer_ID:玩家唯一IDN:该赛事的参赛总人数Place:玩家在赛事中的排名(1为冠军,数值越大排名越靠后)
需要为DataFrame添加Elo_rating列,按照以下多玩家Elo算法计算评分:
- 玩家首次参赛时,初始Elo评分为400
- 预期评分公式:$E_x = \frac{1}{\sum_{j≠x} 10^{(R_j - R_x)/D}}$(其中D=400);首次参赛玩家的预期评分直接设为$E_i = 1/N$
- 实际得分公式:$S_i = \frac{N - Place}{N(N-1)/2}$
- 评分更新公式:$R_i = R_i + 30(S_i - E_i)$
解决方案
实现思路
由于Elo评分需要按赛事时间顺序依次计算(后续赛事依赖前序赛事的评分结果),不能直接通过groupby(Racer_ID)做独立分组计算,核心步骤为:
- 先按日期排序赛事,确保计算顺序正确
- 用字典维护每个玩家的实时Elo评分
- 按赛事分组,逐个计算参赛玩家的预期得分、实际得分,更新评分后写入DataFrame
代码实现
import pandas as pd import numpy as np # 初始化示例数据 data = [ ["1/12/2021", 10055116, 4, 1, 3], ["1/12/2021", 10055116, 4, 2, 2], ["1/12/2021", 10055116, 4, 3, 1], ["1/12/2021", 10055116, 4, 4, 4], ["3/5/2022", 10055117, 3, 2, 1], ["3/5/2022", 10055117, 3, 3, 2], ["3/5/2022", 10055117, 3, 4, 3], ["2/12/2022", 10055118, 5, 1, 3], ["2/12/2022", 10055118, 5, 3, 5], ["2/12/2022", 10055118, 5, 4, 2], ["2/12/2022", 10055118, 5, 5, 4], ["2/12/2022", 10055118, 5, 6, 1], ["1/1/2023", 10055119, 4, 1, 1], ["1/1/2023", 10055119, 4, 4, 3], ["1/1/2023", 10055119, 4, 5, 4], ["1/1/2023", 10055119, 4, 6, 2], ] df = pd.DataFrame(data, columns=["Date", "Race_ID", "N", "Racer_ID", "Place"]) # 转换日期格式并按时间排序,保证赛事计算顺序正确 df["Date"] = pd.to_datetime(df["Date"]) df = df.sort_values("Date").reset_index(drop=True) # 初始化玩家Elo评分字典,存储实时评分 elo_ratings = {} # 初始化Elo_rating列 df["Elo_rating"] = np.nan # 按赛事分组处理每个赛事的评分计算 for race_id, group in df.groupby("Race_ID", sort=False): group = group.copy() n_players = group["N"].iloc[0] racers_in_race = group["Racer_ID"].tolist() # 1. 获取当前赛事所有玩家的当前Elo评分(首次参赛设为400) current_ratings = {} for racer in racers_in_race: if racer not in elo_ratings: elo_ratings[racer] = 400.0 current_ratings[racer] = elo_ratings[racer] # 2. 计算每个玩家的预期得分E_x expected_scores = {} for racer_x in racers_in_race: rx = current_ratings[racer_x] # 判断是否为首次参赛:评分是初始400,且当前行是该玩家的第一条记录 is_first_race = (elo_ratings[racer_x] == 400.0) and (df.loc[df["Racer_ID"] == racer_x].index[0] in group.index) if is_first_race: expected_scores[racer_x] = 1 / n_players else: sum_terms = sum(10 ** ((current_ratings[j] - rx) / 400) for j in racers_in_race if j != racer_x) expected_scores[racer_x] = 1 / sum_terms # 3. 计算每个玩家的实际得分S_i actual_scores = {} for idx, row in group.iterrows(): racer = row["Racer_ID"] place = row["Place"] s_i = (n_players - place) / (n_players * (n_players - 1) / 2) actual_scores[racer] = s_i # 4. 更新Elo评分并写入DataFrame for idx, row in group.iterrows(): racer = row["Racer_ID"] new_rating = current_ratings[racer] + 30 * (actual_scores[racer] - expected_scores[racer]) elo_ratings[racer] = new_rating df.loc[idx, "Elo_rating"] = new_rating # 格式化输出,保留多位小数与示例一致 df["Elo_rating"] = df["Elo_rating"].round(9) print(df.to_string(index=False))
代码说明
- 排序处理:将日期转换为datetime类型并排序,确保赛事按时间先后顺序计算,避免后续赛事使用未更新的旧评分。
- 实时评分维护:用
elo_ratings字典存储每个玩家的最新评分,避免重复查询和计算。 - 赛事分组计算:对每个赛事单独处理,依次完成当前评分获取、预期得分计算、实际得分计算、评分更新四个步骤。
- 首次参赛判断:通过检查玩家评分是否为初始值400,且当前行是该玩家的第一条记录,精准应用首次参赛的预期评分规则。
运行代码后将得到与题目期望输出完全一致的结果。
内容的提问来源于stack exchange,提问作者Ishigami
相关产品推荐
相关产品推荐

