You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

mplsoccer绘制利物浦传球网络球员位置反转问题求助

问题:利物浦传球网络图球员位置左右颠倒

利物浦传球网络图

各位好:
如您所见,这是利物浦某场比赛的传球网络图。对比实际阵容后发现,Trent Alexander-Arnold和Mohamed Salah本应出现在右侧,但图中却在左侧。想请教我哪里出错了?是合并传球者与接球者的环节?分组环节?还是传球绘制环节?

以下是我的代码:

import numpy as np
import pandas as pd
from mplsoccer.pitch import Pitch
from mplsoccer.pitch import VerticalPitch
from matplotlib.colors import to_rgba
from mplsoccer import Pitch, FontManager
import arabic_reshaper
from bidi.algorithm import get_display

#set options to display the mxa_rows and max columns
pd.set_option('display.max_rows', None)
pd.set_option('display.max_columns', None)
#read in csv
dataframe = pd.read_csv('LIVERPOOLvsWOLVES.csv')

#Select only the relevant columns
dataframe = dataframe [["event/eventId","event/timeMin","event/timeSec","event/playerName",
                               "event/playerId","event/outcome","event/x","event/y","event/contestantId",
                               "event/qualifier/0/qualifierId","event/qualifier/0/value",
                               "event/qualifier/1/qualifierId","event/qualifier/1/value",
                               "event/qualifier/2/qualifierId","event/qualifier/2/value",
                               "event/qualifier/3/qualifierId","event/qualifier/3/value",
                               "event/qualifier/4/qualifierId","event/qualifier/4/value",
                               "event/qualifier/5/qualifierId","event/qualifier/5/value",
                               "event/qualifier/6/qualifierId","event/qualifier/6/value",
                               "event/qualifier/7/qualifierId","event/qualifier/7/value",
                               "event/qualifier/8/qualifierId","event/qualifier/8/value",
                               ]]
                               
#Sort now only the wanted team in this case liverpool
dataframe = dataframe[dataframe['event/contestantId']== 'c8h9bw1l82s06h77xxrelzhur']
#Sort now only the successful passes outcome 1
dataframe = dataframe[dataframe['event/outcome']==1]

#rename 22 columns 
dataframe.rename({'event/qualifier/0/qualifierId':'eventqualifier0qualifierId'},axis='columns',inplace=True) + 21 columns


#Add two extra columns
dataframe["endX"]=""
dataframe["endY"]=""

#fill the endX and the endY 
dataframe = pd.DataFrame(dataframe)

#loop through all rows and get the x_end and y_end
def get_final_values(row):
    for i in range(0,8):
        if row[f'eventqualifier{i}qualifierId'] == 140:
            row['endX'] = row[f'eventqualifier{i}value']
        elif row[f'eventqualifier{i}qualifierId'] == 141:
            row['endY'] = row[f'eventqualifier{i}value']
    return row

dataframe = dataframe.apply(get_final_values,axis=1)

#Add two extra columns 
dataframe['passer'] = dataframe['event/playerName']
dataframe['recipient'] = dataframe['event/playerName'].shift(-1)

#1-remove now the dublicated rows  for all the players df.drop(df[(df.score < 50) & (df.score > 20)].index) for player Stefan Bajčetić
dataframe = dataframe.drop(
    dataframe[(dataframe.passer =='Stefan Bajčetić')&(dataframe.recipient =='Stefan Bajčetić')].index)
    
#Remove the empty rows of endX and endY
dataframe = dataframe.drop(
    dataframe[(dataframe.endX =='')&
                                (dataframe.endY =='')].index)
                                
#filter the dataframe untill the first subtitutionOff
dataframe = dataframe[
    dataframe['event/timeMin']<75]
    
#Filter now again only the needed columns
dataframe = dataframe [["event/eventId","event/timeMin","event/timeSec","event/playerName",
                        "event/playerId","event/outcome","x","y","event/contestantId",
                        "endX","endY","passer","recipient"
                               ]]
                               
#Rename some columns
dataframe.rename({'event/playerName':'playerName'},axis='columns',inplace=True)
dataframe.rename({'event/playerId':'playerId'},axis='columns',inplace=True)

#Create the average location
average_locations = dataframe.groupby('passer').agg({'x':['mean'],'y':['mean','count']})
average_locations.columns = ['x','y','count']

#find the number of passes between each player
#pass_between = sorted_df_SuccessfullPasses[sorted_df_SuccessfullPasses['event/timeMin']<75]

pass_between = dataframe.groupby(['passer','recipient'], as_index=False).playerId.count().reset_index()
pass_between.rename({'playerId':'pass_count'},axis='columns',inplace=True)

#Merge now the average location and pass_between
pass_between = pass_between.merge(average_locations, left_on='passer', right_index=True)
pass_between = pass_between.merge(average_locations, left_on='recipient', right_index=True,suffixes=['','_end'])

MAX_LINE_WIDTH = 18
MAX_MARKER_SIZE = 3000
pass_between['width'] = (pass_between.pass_count / pass_between.pass_count.max() * MAX_LINE_WIDTH)
average_locations['marker_size'] = (average_locations['count']
                                         / average_locations['count'].max() * MAX_MARKER_SIZE)
                                          
MIN_TRANSPARENCY = 0.3
color = np.array(to_rgba('white'))
color = np.tile(color, (len(pass_between), 1))
c_transparency = pass_between.pass_count / pass_between.pass_count.max()
c_transparency = (c_transparency * (1 - MIN_TRANSPARENCY)) + MIN_TRANSPARENCY
color[:, 3] = c_transparency

#Now plotting
pitch = Pitch(pitch_type='statsbomb', pitch_color='black', line_color='#c7d5cc')

fig, ax = pitch.draw(figsize=(16, 11), constrained_layout=True, tight_layout=False)

fig.set_facecolor("#22312b")

arrows = pitch.arrows(1.2*pass_between.x,    .8*pass_between.y,
                      1.2*pass_between.x_end,.8*pass_between.y_end, ax=ax,
                     width = 3, headwidth= 3, color='white', zorder =1, alpha = .5)

arrows = pitch.lines(1.2*pass_between.x,    .8*pass_between.y,
                     1.2*pass_between.x_end,.8*pass_between.y_end,lw=pass_between.width,
                         color=color, zorder=1, ax=ax)
    
nodes = pitch.scatter(1.2*average_locations.x,.8*average_locations.y,
                     s = average_locations.marker_size, color = 'red', edgecolors = 'black', linewidth = 2.5, alpha = 1, zorder = 1, ax=ax)
                       
for index, row in average_locations.iterrows():
    pitch.annotate(row.name, xy=(1.2*row.x, .8*row.y), c='white', va='center',
                   ha='center', size=16, weight='bold', ax=ax)   

问题诊断与解决

问题出在绘制环节的坐标错误缩放,你手动对x、y坐标进行了1.2*和.8*的缩放操作,这既扭曲了球场比例,又导致球员位置偏移颠倒。此外代码中还有一处语法错误需要修复:

1. 修复坐标缩放问题

移除所有手动缩放坐标的代码,mplsoccer的Pitch类会自动适配StatsBomb的0-100坐标系。修改后的绘制代码如下:

# 移除手动缩放,直接使用原始坐标
arrows = pitch.arrows(pass_between.x, pass_between.y,
                      pass_between.x_end, pass_between.y_end, ax=ax,
                      width=3, headwidth=3, color='white', zorder=1, alpha=.5)

arrows = pitch.lines(pass_between.x, pass_between.y,
                     pass_between.x_end, pass_between.y_end, lw=pass_between.width,
                     color=color, zorder=1, ax=ax)

nodes = pitch.scatter(average_locations.x, average_locations.y,
                     s=average_locations.marker_size, color='red', edgecolors='black', linewidth=2.5, alpha=1, zorder=1, ax=ax)

for index, row in average_locations.iterrows():
    pitch.annotate(row.name, xy=(row.x, row.y), c='white', va='center',
                   ha='center', size=16, weight='bold', ax=ax)

如果修改后位置仍颠倒,说明数据是从对手视角记录的,创建Pitch时添加reverse=True翻转视角:

pitch = Pitch(pitch_type='statsbomb', pitch_color='black', line_color='#c7d5cc', reverse=True)

2. 修复列重命名语法错误

你代码中dataframe.rename(...) + 21 columns是无效语法,替换为批量重命名逻辑:

# 批量重命名qualifier相关列
for i in range(9):
    dataframe.rename({f'event/qualifier/{i}/qualifierId': f'eventqualifier{i}qualifierId',
                      f'event/qualifier/{i}/value': f'eventqualifier{i}value'},
                     axis='columns', inplace=True)

这一步修复后,后续获取传球终点坐标的逻辑才能正常工作。

内容的提问来源于stack exchange,提问作者WilliamAshoti

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 02:12:24