动图,用Python追踪NBA球员的运动轨迹

    xiaoxiao2023-12-05  146

    在本文中,我将介绍如何在stats.nba.com上的比赛运动动画中提取一些额外的信息。

    In[1]:

    In[2]:

    我们将从一场比赛中提取信息。这是快船队(Clippers)和火箭队(Rockets)在季后系列赛的第5场比赛。在比赛中,James Harden瓦解了快船队的防守,冲向篮筐,把球传给Trevor Ariza,轻松获得3分。

    我已经在下面嵌入了运动动画。

    In[3]:

    输出是一个动画

    获取数据

    下面是我们从stats.nba.com的应用程序接口获取数据的链接。链接里有2个参数:eventid是特定比赛的IDgameid则是季后赛的ID

    In[4]:

    我们发出请求提取数据

    In[5]:

    Out[5]:

    我们想要的数据在:home (主场球员的数据) visitors (客场球员的数据),和 moments (使用于动画中用于绘制球员运动轨迹信息的数据)

    In[6]:

    让我们看看home字典里包含的信息。

    In [7]:

    home

    Out[7]:

    Visitor字典包含了关于快船队的同样类型的信息。

    In[8]:

    visitor

    Out[8]:

    现在,让我们看看moments列表。

    In [9]:

    # 检查长度

    len(moments)

    Out[9]:

    700

    长度告诉我们,上面的动画由700个项目/时刻组成。但是,都有些什么信息呢?让我们来看看第一个。

    In [10]:

    moments[0]

    Out[10]:

    [3,

    1431486313010,

    715.32,

    19.0,

    None,

    [[-1, -1, 43.51745, 10.76997, 1.11823],

    [1610612745, 1891, 43.21625, 12.9461, 0.0],

    [1610612745, 2772, 90.84496, 7.79534, 0.0],

    [1610612745, 2730, 77.19964, 34.36718, 0.0],

    [1610612745, 2746, 46.24382, 21.14748, 0.0],

    [1610612745, 201935, 81.0992, 48.10742, 0.0],

    [1610612746, 2440, 88.12605, 11.23036, 0.0],

    [1610612746, 200755, 84.41011, 43.47075,0.0],

    [1610612746, 101108, 46.18569, 16.49072,0.0],

    [1610612746, 201599, 78.64683, 31.87798,0.0],

    [1610612746, 201933, 65.89714, 25.57281,0.0]]]

    首先,我们看到moments中的时刻或项目是一个包含了一堆信息的列表。我们逐一查看列表中的每一个项目。

    1 moments[0]中的第1项是这一刻所发生的时期或季度。

    2 Unix时间。(是一种时间表示方式,定义为从格林威治时间197011日(UTC/GMT的午夜)开始所经过的秒数,不考虑闰秒——译者注)

    3 3项是指比赛剩下的时间。

    4 4项是指计时器剩下的时间。

    5 我不知道第5项代表什么。

    6 6项是由11个子列表组成的列表,每个子列表包含球场上某个球员或球的坐标。

    6.1 11个子列表中的第1个包含了球的信息。

    6.1.1 2项是表示teamidplayerid的值,用于表明该列表是关于球的信息。

    6.1.2 接下来的2项则是xy坐标值,用于表示球场中球的位置。

    6.1.3 5项(最后一项)是代表球的半径。这个值在整个动画中都随着球的高度而变化。半径越大,球就越高。因此,如果球员投篮,球的大小就会增加,在拍摄弓的顶点达到其最大值,然后随着高度下降,球逐渐变小。

    6.2 6项中的10个列表表示球场上的10名球员。在这些列表中,关于球的信息是一样的。

    6.2.1 2项是teamidplayerid,表示这是某个特定球员的列表。

    6.2.2 接下来的2项则是xy坐标值,代表该球员在球场上的位置。

    6.2.3 最后1项是球员的活动半径,这是不相关的信息。

    现在我们对moments数据所代表的信息有了一定的理解。我们把它输入pandas DataFrame

    首先我们创建DataFrame的列标签。

    In[11]:

    # 列标签

    essay-headers =["team_id", "player_id", "x_loc","y_loc",

    "radius","moment", "game_clock", "shot_clock"]

    然后,我们为每个球员创建一个包含moments数据的单独列表。

    In[12]:

    # 初始化新列表player_moments

    player_moments= []

    for momentin moments:

    # 对列表中的每个球员/进球找到相应的moments

    for player in moment[5]:

    #对每个球员/进球增加额外的信息,包括每个moment的索引,比赛时间,投篮时间

    player.extend((moments.index(moment),moment[2], moment[3]))

    player_moments.append(player)

    In[13]:

    # 查看moments列表

    player_moments[0:11]

    Out[13]:

    [[-1, -1,43.51745, 10.76997, 1.11823, 0, 715.32, 19.0],

    [1610612745, 1891, 43.21625, 12.9461, 0.0, 0,715.32, 19.0],

    [1610612745, 2772, 90.84496, 7.79534, 0.0, 0,715.32, 19.0],

    [1610612745, 2730, 77.19964, 34.36718, 0.0, 0,715.32, 19.0],

    [1610612745, 2746, 46.24382, 21.14748, 0.0, 0,715.32, 19.0],

    [1610612745, 201935, 81.0992, 48.10742, 0.0,0, 715.32, 19.0],

    [1610612746, 2440, 88.12605, 11.23036, 0.0, 0,715.32, 19.0],

    [1610612746, 200755, 84.41011, 43.47075, 0.0,0, 715.32, 19.0],

    [1610612746, 101108, 46.18569, 16.49072, 0.0,0, 715.32, 19.0],

    [1610612746, 201599, 78.64683, 31.87798, 0.0,0, 715.32, 19.0],

    [1610612746, 201933, 65.89714, 25.57281, 0.0,0, 715.32, 19.0]]

    将刚刚创建的moments列表和我们的列标签传给pd.DataFrame,创建我们的DataFrame

    In[14]:

    #Player_moments列表转化为数据框形式

    df =pd.DataFrame(player_moments, columns=essay-headers)

    In [15]:

    df.head(11)

    Out[15]:

    我们还没有完成。我们应添加包含球员姓名和球衣号码的列。首先,将所有的球员放入一个列表。

    In[16]:

    # 创建player列表,将主队运动员的数据赋值给player

    players =home["players"]

    # 添加客队运动员的数据

    players.extend(visitor["players"])

    利用players列表,我们可以创建一个以球员ID作为关键字的字典和一个包含球员姓名和球衣号码的值列表。

    In[17]:

    # 创建新的字典id_dict

    id_dict ={}

    # 在字典中增加我们想要的值,

    for playerin players:

    id_dict[player['playerid']] =[player["firstname"]+""+player["lastname"],player["jersey"]]

    In [18]:

    id_dict

    Out[18]:

    {1891:['Jason Terry', '31'],

    2037: ['Jamal Crawford', '11'],

    2045: ['Hedo Turkoglu', '15'],

    2440: ['Matt Barnes', '22'],

    2563: ['Dahntay Jones', '31'],

    2730: ['Dwight Howard', '12'],

    2746: ['Josh Smith', '5'],

    2772: ['Trevor Ariza', '1'],

    101108: ['Chris Paul', '3'],

    200755: ['JJ Redick', '4'],

    201147: ['Corey Brewer', '33'],

    201150: ['Spencer Hawes', '10'],

    201175: ['Glen Davis', '0'],

    201595: ['Joey Dorsey', '8'],

    201599: ['DeAndre Jordan', '6'],

    201933: ['Blake Griffin', '32'],

    201935: ['James Harden', '13'],

    201991: ['Lester Hudson', '14'],

    202327: ['Ekpe Udoh', '13'],

    203085: ['Austin Rivers', '25'],

    203093: ['Terrence Jones', '6'],

    203123: ['Kostas Papanikolaou', '16'],

    203143: ['Pablo Prigioni', '9'],

    203909: ['KJ McDaniels', '32'],

    203910: ['Nick Johnson', '3'],

    203991: ['Clint Capela', '15']}

    我们更新id_dict,纳入球的ID

    In[19]:

    #将球的ID纳入字典中

    id_dict.update({-1:['ball', np.nan]})

    然后利用位映象法对应player_id列创建player_name列和一个player_jersey。我们用lambda,位映像一个匿名函数,根据传给函数的player_id值而返回正确的player_nameplayer_jersey

    换句话说,下面的代码所做的是遍历player_id列中的球员ID,然后把每个球员ID传递给那个匿名函数。这个函数返回的是球员的名字以及该球员的球衣号码,并把这些值添加到我们的DataFrame中。

    In [20]:

    #创建与player_id匹配的player_nameplayer_jersey

    df["player_name"]= df.player_id.map(lambda x: id_dict[x][0])

    df["player_jersey"]= df.player_id.map(lambda x: id_dict[x][1])

    In [21]:

    #显示df数据框的前11

    df.head(11)

    Out[21]:

    绘制运动轨迹

    绘制James Harden在整个比赛中的运动轨迹。我们可以借助从stas.nba.com获取的球场图片来绘制球场。你可以在上面找到SVG。我用matplotlib将其转换成PNG文件,从而使其更容易绘制。此外,还应注意xy轴上每1单位代表篮球场上的1英尺。

    In[22]:

    #获取 Harden的运动轨迹

    harden =df[df.player_name=="James Harden"]

    # 读取fullcourt.png图片

    court =plt.imread("fullcourt.png")

    In [23]:

    plt.figure(figsize=(15,11.5))

    #画出movemnts的散点图

    #使用colormap函数来表示比赛时的变化

    plt.scatter(harden.x_loc,harden.y_loc, c=harden.game_clock,

    cmap=plt.cm.Blues, s=1000,zorder=1)

    #颜色越深表示比赛中运动的时间越早

    cbar =plt.colorbar(orientation="horizontal")

    cbar.ax.invert_xaxis()

    #画篮球场

    # zorder=0 Harden's movements 运动轨迹下面设置界线

    # 原始的动画是在SVG 坐标空间中画出的,x=0,y=0设置为左上

    # 所以在这幅图中以相同的方式设置x,y轴的值.

    #在列表中我们用[0,94]表示球场x轴的范围,[50,0]表示球场y轴的范围

    plt.imshow(court,zorder=0, extent=[0,94,50,0])

    plt.xlim(0,101)

    plt.show()

    我们也可以通过matplotlib Patches重新创建大部分的球场。我们不是使用SVG坐标系统,而是使用经典的直角坐标系统。因此,我们的y轴值将是负值,而非正值。

    In[24]:

    frommatplotlib.patches import Circle, Rectangle, Arc

    #draw_court函数用来画篮球场线

    defdraw_court(ax=None, color="gray", lw=1, zorder=0):

    if ax is None:

    ax = plt.gca()

    #在球场周围画出出界的界线

    outer = Rectangle((0,-50), width=94,height=50, color=color,

    zorder=zorder,fill=False, lw=lw)

    #画左、右两侧的篮球场框

    l_hoop = Circle((5.35,-25), radius=.75,lw=lw, fill=False,

    color=color, zorder=zorder)

    r_hoop = Circle((88.65,-25), radius=.75,lw=lw, fill=False,

    color=color, zorder=zorder)

    #画左、右侧的篮板

    l_backboard = Rectangle((4,-28), 0, 6,lw=lw, color=color,

    zorder=zorder)

    r_backboard = Rectangle((90, -28), 0, 6,lw=lw,color=color,

    zorder=zorder)

    #画左、右两侧禁区

    l_outer_box = Rectangle((0, -33), 19, 16,lw=lw, fill=False,

    color=color,zorder=zorder)

    l_inner_box = Rectangle((0, -31), 19, 12,lw=lw, fill=False,

    color=color,zorder=zorder)

    r_outer_box = Rectangle((75, -33), 19, 16,lw=lw, fill=False,

    color=color,zorder=zorder)

    r_inner_box = Rectangle((75, -31), 19, 12,lw=lw, fill=False,

    color=color,zorder=zorder)

    #画左、右两侧罚球圈

    l_free_throw = Circle((19,-25), radius=6,lw=lw, fill=False,

    color=color,zorder=zorder)

    r_free_throw = Circle((75, -25), radius=6,lw=lw, fill=False,

    color=color,zorder=zorder)

    # 画左右两侧角落区域三分线

    # a代表上边界

    # b 代表下边界

    l_corner_a = Rectangle((0,-3), 14, 0,lw=lw, color=color,

    zorder=zorder)

    l_corner_b = Rectangle((0,-47), 14, 0,lw=lw, color=color,

    zorder=zorder)

    r_corner_a = Rectangle((80, -3), 14, 0,lw=lw, color=color,

    zorder=zorder)

    r_corner_b = Rectangle((80, -47), 14, 0,lw=lw, color=color,

    zorder=zorder)

    #画左、右两侧三分线的弧

    l_arc = Arc((5,-25), 47.5, 47.5,theta1=292, theta2=68, lw=lw,

    color=color, zorder=zorder)

    r_arc = Arc((89, -25), 47.5, 47.5,theta1=112, theta2=248, lw=lw,

    color=color, zorder=zorder)

    #画出半球场

    half_court = Rectangle((47,-50), 0, 50,lw=lw, color=color,

    zorder=zorder)

    hc_big_circle = Circle((47, -25), radius=6,lw=lw, fill=False,

    color=color,zorder=zorder)

    hc_sm_circle = Circle((47, -25), radius=2,lw=lw, fill=False,

    color=color,zorder=zorder)

    court_elements = [l_hoop, l_backboard,l_outer_box, outer,

    l_inner_box,l_free_throw, l_corner_a,

    l_corner_b, l_arc,r_hoop, r_backboard,

    r_outer_box, r_inner_box,r_free_throw,

    r_corner_a, r_corner_b,r_arc, half_court,

    hc_big_circle,hc_sm_circle]

    #x轴上增加court元素

    for element in court_elements:

    ax.add_patch(element)

    return ax

    In [25]:

    plt.figure(figsize=(15,11.5))

    # 画出movemnts 的散点图

    # 使用colormap表示比赛时的变化

    plt.scatter(harden.x_loc,-harden.y_loc, c=harden.game_clock,

    cmap=plt.cm.Blues, s=1000,zorder=1)

    # 颜色越深表示移动轨迹越早

    cbar =plt.colorbar(orientation="horizontal")

    # 逆转colorbar让左侧区域有较高的值

    cbar.ax.invert_xaxis()

    draw_court()

    plt.xlim(0,101)

    plt.ylim(-50,0)

    plt.show()

    计算运动距离

    通过得到连续点之间的欧式距离(Euclidean distance是一个通常采用的距离定义,它是在m维空间中两个点之间的真实距离。——译者注),然后对这些距离求和,我们可以算出一个球员运动的距离。

    In[26]:

    deftravel_dist(player_locations):

    # 对每一列差分相减求误差

    diff = np.diff(player_locations, axis=0)

    # 将误差平方相加再开方

    dist = np.sqrt((diff ** 2).sum(axis=1))

    # 返回所有距离的和

    returndist.sum()

    In [27]:

    # Harden'的运动距离

    dist =travel_dist(harden[["x_loc", "y_loc"]])

    dist

    Out[27]:

    197.44816608512659

    我们可以使用groupbyapply得到每个球员的运动总距离。我们将球员分组,得到他们每个的坐标位置,然后应用上述距离函数。

    In[28]:

    #使用groupbyapply得到每个球员的运动总距离

    player_travel_dist= df.groupby('player_name')[['x_loc', 'y_loc']].apply(travel_dist)

    player_travel_dist

    Out[28]:

    player_name

    BlakeGriffin 153.076637

    ChrisPaul 176.198330

    DeAndreJordan 119.919877

    DwightHoward 123.439590

    JJRedick 184.504145

    JamesHarden 197.448166

    JasonTerry 173.308880

    JoshSmith 162.226100

    MattBarnes 161.976406

    TrevorAriza 153.389365

    ball 328.317612

    dtype:float64

    计算平均速度

    计算球员的平均速度相当简单:只需将距离除以时间即可。

    In[29]:

    # 获取比赛的时长

    seconds =df.game_clock.max() - df.game_clock.min()

    # 以每秒英尺为单位

    harden_fps= dist / seconds

    # 转化为每小时英里为单位

    harden_mph= 0.681818 * harden_fps

    harden_mph

    Out[29]:

    4.7977089702005902

    我们可以使用先前创建的player_travel_dist得到每个球员的平均速度。

    In[30]:

    #计算每个球员的平均速度

    player_speeds= (player_travel_dist/seconds) * 0.681818

    player_speeds

    Out[30]:

    player_name

    BlakeGriffin 3.719544

    ChrisPaul 4.281368

    DeAndreJordan 2.913882

    DwightHoward 2.999406

    JJRedick 4.483188

    JamesHarden 4.797709

    JasonTerry 4.211159

    JoshSmith 3.941863

    MattBarnes 3.935796

    TrevorAriza 3.727143

    ball 7.977650

    dtype:float64

    计算球员之间的距离

    让我们来看看比赛中Harden与其他每一个球员之间的距离。

    首先得到Harden的位置。

    In[31]:

    #获取Harden的位置

    harden_loc= df[df.player_name=="James Harden"][["x_loc","y_loc"]]

    In [32]:

    harden_loc.head()

    Out[32]:

    x_loc

    y_loc

    5

    81.09920

    48.10742

    16

    81.01996

    48.11580

    27

    80.93976

    48.12279

    38

    80.85964

    48.12597

    49

    80.77435

    48.12823

    现在,我们以player_name分组,得到每个球员和球的位置。

    In[33]:

    group =df[df.player_name!="JamesHarden"].groupby("player_name")[["x_loc","y_loc"]]

    我们利用scipy库中的欧几里德函数,在分组中使用它。函数返回一张列表,包含整个比赛中James Harden和其他球员之间的距离。

    In[34]:

    fromscipy.spatial.distance import euclidean

    In [35]:

    #在每个moment计算每个球员之间的距离

    defplayer_dist(player_a, player_b):

    return [euclidean(player_a.iloc[i],player_b.iloc[i])

    for i in range(len(player_a))]

    每个球员的位置传给player_dist函数里的player_a参数,Harden的位置传给player_b参数。

    In[36]:

    harden_dist= group.apply(player_dist, player_b=(harden_loc))

    In [37]:

    harden_dist

    Out[37]:

    player_name

    BlakeGriffin [27.182922508363593,27.055820685362697, 26.94...

    ChrisPaul [47.10168680005101,46.861684798626264, 46.618...

    DeAndreJordan [16.413678482610162,16.48314022711995, 16.556...

    DwightHoward [14.282883583198455,14.35720390798292, 14.433...

    JJRedick [5.697440979685529,5.683098128626677, 5.67370...

    JasonTerry [51.685939334067434,51.40228120171322, 51.096...

    JoshSmith [44.06513224475787,43.81023267813696, 43.5637...

    MattBarnes [37.5405670597302,37.59395273374297, 37.68516...

    TrevorAriza [41.47340873263252,41.414794206955804, 41.348...

    ball [52.976156009708745,52.70430545836839, 52.435...

    dtype:object

    只需注意球在其列表中仅有690个项目,而球员则有700项。

    In[38]:

    len(harden_dist["ball"])

    Out[38]:

    690

    In [39]:

    len(harden_dist["BlakeGriffin"])

    Out[39]:

    700

    现在,我们知道如何得到球员之间的距离,让我们试着看看James Harden的运球对球场上一些空间的影响。

    让我们再看一看运动画面,看看在Harden运球过程中发生的情况。

    In[40]:

    IFrame('http://stats.nba.com/movement/#!/?GameID=0041400235&GameEventID;=308',width=700,height=400)

    Out[40]:

    Harden突入篮下,DeAndre Jordan移出Dwight Howards防守的篮下。Matt Barnes切换过来防守Howards(但跌倒),让Ariza攻破。Harden看到Ariza,把球传给了他,Chris Paul试图冲过来防守,Ariza把握住了机会,投篮成功,这一切都发生在第三季度约11分钟46秒到11分钟42秒之间。计时钟从约10.1秒开始计时,那时Harden开始运球,计时到约62秒时,Ariza投出球。实际上,我们可以在Ariza的投篮日志页找到更多关于他投球尝试的信息。

    In[41]:

    #获取特定时间段的数据

    time_mask =(df.game_clock <= 706) & (df.game_clock >= 702) & \

    (df.shot_clock <= 10.1) &(df.shot_clock >= 6.2)

    time_df =df[time_mask]

    从动画看,Harden 似乎在第四节比赛只剩7.77.8秒的时候传了球。我们可以通过查看他和球之间的距离来确认这一情况。

    In[42]:

    # 计算James Harden与球之间的距离

    ball =time_df[time_df.player_name=="ball"]

    harden2 =time_df[time_df.player_name=="James Harden"]

    harden_ball_dist= player_dist(ball[["x_loc", "y_loc"]],

    harden2[["x_loc","y_loc"]])

    In [43]:

    #画出Harden与球之间距离曲线图

    plt.figure(figsize=(12,9))

    x =time_df.shot_clock.unique()

    y = harden_ball_dist

    plt.plot(x,y)

    plt.xlim(8,7)

    plt.xlabel("ShotClock")

    plt.ylabel("Distancebetween Harden and the Ball (feet)")

    plt.vlines(7.7,0, 30, color='gray', lw=0.7)

    plt.show()

    让我们绘制在这段时间内的一些球员之间的距离变化。我们将绘制HardenJordanHowardBarnesArizaBarnesArizaPaul之间的距离变化。

    In[44]:

    # 获取我们想要的球员信息

    player_mask= (time_df.player_name=="Trevor Ariza") | \

    (time_df.player_name=="DeAndre Jordan") | \

    (time_df.player_name=="DwightHoward") | \

    (time_df.player_name=="MattBarnes") | \

    (time_df.player_name=="ChrisPaul") | \

    (time_df.player_name=="JamesHarden")

    In [45]:

    # 获取球员的位置

    group2 =time_df[player_mask].groupby('player_name')[["x_loc","y_loc"]]

    In [46]:

    # 获取球员之间的距离差

    harden_jordan= player_dist(group2.get_group("James Harden"),

    group2.get_group("DeAndre Jordan"))

    howard_barnes= player_dist(group2.get_group("Dwight Howard"),

    group2.get_group("Matt Barnes"))

    ariza_barnes= player_dist(group2.get_group("Trevor Ariza"),

    group2.get_group("Matt Barnes"))

    ariza_paul= player_dist(group2.get_group("Trevor Ariza"),

    group2.get_group("ChrisPaul"))

    In [47]:

    # 创建距离列表帮助我们作图

    distances =[ariza_barnes, ariza_paul, harden_jordan, howard_barnes]

    # 对每条线贴标签t

    labels =["Ariza - Barnes", "Ariza - Paul", "Harden -Jordan", "Howard - Barnes"]

    #对每条线着色

    colors = sns.color_palette('colorblind',4)

    plt.figure(figsize=(12,9))

    #使用枚举来索引标签、颜色与匹配的距离数据

    for i, distin enumerate(distances):

    plt.plot(time_df.shot_clock.unique(), dist,color=colors[i])

    y_pos = dist[-1]

    plt.text(6.15, y_pos, labels[i], fontsize=14,color=colors[i])

    # 画出Harden传球的线

    plt.vlines(7.7,0, 30, color='gray', lw=0.7)

    plt.annotate("Hardenpasses the ball", (7.7, 27),

    xytext=(8.725, 26.8), fontsize=12,

    arrowprops=dict(facecolor='lightgray', shrink=0.10))

    # 创建水平网格线

    plt.grid(axis='y',color='gray',linestyle='--', lw=0.5, alpha=0.5)

    plt.xlim(10.1,6.2)

    plt.title("TheDistance (in feet) Between Players \nFrom the Beginning"

    " of Harden's Drive up untilAriza Releases his Shot", size=16)

    plt.xlabel("TimeLeft on Shot Clock (seconds)", size=14)

    # 去掉多余的线

    sns.despine(left=True,bottom=True)

    plt.show()

    原文发布时间为:2015-10-13

    本文来自云栖社区合作伙伴“大数据文摘”,了解相关信息可以关注“BigDataDigest”微信公众号

    相关资源:论文研究-一种角色运动轨迹提取及重定向的方法.pdf
    最新回复(0)