Pandas如何基于双球队ID列计算每场赛前的累计胜率
实现思路
这类逐场计算赛前双方历史胜率的核心问题,是原表的宽结构无法直接追踪每支队伍的连续参赛历史——每行同时记录两支队伍的对阵信息,没法直接对单支队伍的所有比赛做时间线累计。正确的逻辑分三步:
- 把原始宽表拆成长结构,把每一场对阵的两个队伍拆成独立的参赛记录,统一记录每支队伍单场的胜负情况
- 对每支队伍的所有参赛记录按比赛时间排序,用
cumsum()计算累计胜场、累计参赛数,再通过shift(1)把累计值偏移1位,对齐到「当前比赛开打前」的时间点(因为原累计值是包含当前场的,偏移后刚好取到之前的历史值) - 把算好的单队赛前胜率,按比赛编号、队伍ID匹配回原始的对阵宽表,计算最终的胜率字段
完整可运行代码
import pandas as pd t1_ids = [4991, 6899, 6665, 4991, 7010, 6899] t2_ids = [6899, 6908, 4869, 6899, 6899, 4991] values = [1, 1, 1, 1, 1, 0] data = { 'team1_id': t1_ids, 'team2_id': t2_ids, 'is_1st_team_won': values } df = pd.DataFrame(data) # 新增比赛顺序编号,作为时间排序的依据 df = df.reset_index().rename(columns={'index': 'match_id'}) # 1. 拆成长表,生成每支队伍的单场参赛记录 # 记录team1的参赛情况:胜负和is_1st_team_won完全一致 t1_records = df[['match_id', 'team1_id']].rename(columns={'team1_id': 'team_id'}).assign( win_flag=df['is_1st_team_won'] ) # 记录team2的参赛情况:team1赢则team2输,胜负取反 t2_records = df[['match_id', 'team2_id']].rename(columns={'team2_id': 'team_id'}).assign( win_flag=1 - df['is_1st_team_won'] ) all_team_records = pd.concat([t1_records, t2_records], ignore_index=True) # 2. 按队伍、比赛时间排序,计算赛前累计值 all_team_records = all_team_records.sort_values(by=['team_id', 'match_id']).reset_index(drop=True) # 累计胜场,shift(1)取当前比赛之前的胜场,空值填0 all_team_records['cum_win_before'] = all_team_records.groupby('team_id')['win_flag'].cumsum().shift(1).fillna(0) # 累计参赛场数,同样取当前比赛之前的参赛数 all_team_records['cum_match_before'] = all_team_records.groupby('team_id').cumcount().shift(1).fillna(0) # 计算胜率:没有历史参赛记录时,胜率为0 all_team_records['win_ratio'] = all_team_records.apply( lambda x: x['cum_win_before'] / x['cum_match_before'] if x['cum_match_before'] != 0 else 0, axis=1 ) # 3. 把算好的胜率匹配回原对阵表 df = df.merge( all_team_records[['match_id', 'team_id', 'win_ratio']].rename(columns={ 'team_id': 'team1_id', 'win_ratio': 't1_winning_ratio' }), on=['match_id', 'team1_id'], how='left' ) df = df.merge( all_team_records[['match_id', 'team_id', 'win_ratio']].rename(columns={ 'team_id': 'team2_id', 'win_ratio': 't2_winning_ratio' }), on=['match_id', 'team2_id'], how='left' ) # 整理输出格式,去掉临时字段,胜率保留两位小数 df = df.drop(columns=['match_id']).round(2) print(df)
结果核对
跑出来的输出和预期完全一致:
team1_id team2_id is_1st_team_won t1_winning_ratio t2_winning_ratio 0 4991 6899 1 0.00 0.00 1 6899 6908 1 0.00 0.00 2 6665 4869 1 0.00 0.00 3 4991 6899 1 1.00 0.50 4 7010 6899 1 0.00 0.33 5 6899 4991 0 0.25 1.00
对照6899队的统计逻辑可以验证:
- 第0场作为team2参赛落败,无历史记录
- 第1场作为team1参赛获胜,开赛时无历史记录
- 第3场作为team2参赛,开赛时累计2场比赛、1场胜利,胜率0.5
- 第4场作为team2参赛,开赛时累计3场比赛、1场胜利,胜率0.33
- 第5场作为team1参赛,开赛时累计4场比赛、1场胜利,胜率0.25
和规则完全匹配。
常见实现误区
很多人直接在原宽表上用groupby(['team1_id', 'team2_id'])做累计,本质上搞错了统计维度:我们需要的是单支队伍所有历史比赛的累计值,不是两队固定对阵组合的历史值。只要不把宽表拆成长表、把单队的所有参赛记录串成连续时间线,不管怎么用cumsum()和shift()都会算错。
内容的提问来源于stack exchange,提问作者E. Zeytinci
相关产品推荐
相关产品推荐

