Visualize pre-snap positions and player movement

Interestingly, the NFL data set includes data on player movement within each football play. Visualizing the changes in your time-series data can often provide even more insight. In this section, we will use pandas and matplotlib to visually depict a play during the season.

Install pandas and matplotlib:

  1. pip install pandas matplotlib

Draw football field

  1. def generate_field():
  2. """Generates a realistic american football field with line numbers and hash marks.
  3. Returns:
  4. [tuple]: (figure, axis)
  5. """
  6. rect = patches.Rectangle((0, 0), 120, 53.3, linewidth=2,
  7. edgecolor='black', facecolor='green', zorder=0)
  8. fig, ax = plt.subplots(1, figsize=(12, 6.33))
  9. ax.add_patch(rect)
  10. # line numbers
  11. plt.plot([10, 10, 20, 20, 30, 30, 40, 40, 50, 50, 60, 60, 70, 70, 80,
  12. 80, 90, 90, 100, 100, 110, 110, 120, 0, 0, 120, 120],
  13. [0, 53.3, 53.3, 0, 0, 53.3, 53.3, 0, 0, 53.3, 53.3, 0, 0, 53.3,
  14. 53.3, 0, 0, 53.3, 53.3, 0, 0, 53.3, 53.3, 53.3, 0, 0, 53.3],
  15. color='white')
  16. for x in range(20, 110, 10):
  17. numb = x
  18. if x > 50:
  19. numb = 120-x
  20. plt.text(x, 5, str(numb - 10), horizontalalignment='center', fontsize=20, color='white')
  21. plt.text(x-0.95, 53.3-5, str(numb-10),
  22. horizontalalignment='center', fontsize=20, color='white',rotation=180)
  23. # hash marks
  24. for x in range(11, 110):
  25. ax.plot([x, x], [0.4, 0.7], color='white')
  26. ax.plot([x, x], [53.0, 52.5], color='white')
  27. ax.plot([x, x], [22.91, 23.57], color='white')
  28. ax.plot([x, x], [29.73, 30.39], color='white')
  29. # set limits and hide axis
  30. plt.xlim(0, 120)
  31. plt.ylim(-5, 58.3)
  32. plt.axis('off')
  33. return fig, ax

Draw players’ movement based on game_id and play_id

  1. conn = psycopg2.connect(database="db",
  2. host="host",
  3. user="user",
  4. password="pass",
  5. port="111")
  6. def draw_play(game_id, play_id, home_label='position', away_label='position', movements=False):
  7. """Generates a chart to visualize player pre-snap positions and
  8. movements during the given play.
  9. Args:
  10. game_id (int)
  11. play_id (int)
  12. home_label (str, optional): Default is 'position' but can be 'displayname'
  13. or other column name available in the table.
  14. away_label (str, optional): Default is 'position' but can be 'displayname'
  15. or other column name available in the table.
  16. movements (bool, optional): If False, only draws the pre-snap positions.
  17. If True, draws the movements as well.
  18. """
  19. # query all tracking data for the given play
  20. sql = "SELECT * FROM tracking WHERE gameid={game} AND playid={play} AND team='home'"\
  21. .format(game=game_id, play=play_id)
  22. home_team = pd.read_sql(sql, conn)
  23. sql = "SELECT * FROM tracking WHERE gameid={game} AND playid={play} AND team='away'"\
  24. .format(game=game_id, play=play_id)
  25. away_team = pd.read_sql(sql, conn)
  26. # generate the football field
  27. fig, ax = generate_field()
  28. # query pre_snap player positions
  29. home_pre_snap = home_team.query('event == "ball_snap"')
  30. away_pre_snap = away_team.query('event == "ball_snap"')
  31. # visualize pre-snap positions with scatter plot
  32. home_pre_snap.plot.scatter(x='x', y='y', ax=ax, color='yellow', s=35, zorder=3)
  33. away_pre_snap.plot.scatter(x='x', y='y', ax=ax, color='blue', s=35, zorder=3)
  34. # annotate the figure with the players' position or name
  35. # (depending on the *label* parameter's value)
  36. home_positions = home_pre_snap[home_label].tolist()
  37. away_positions = away_pre_snap[away_label].tolist()
  38. for i, pos in enumerate(home_positions):
  39. ax.annotate(pos, (home_pre_snap['x'].tolist()[i], home_pre_snap['y'].tolist()[i]))
  40. for i, pos in enumerate(away_positions):
  41. ax.annotate(pos, (away_pre_snap['x'].tolist()[i], away_pre_snap['y'].tolist()[i]))
  42. if movements:
  43. # visualize player movements for home team
  44. home_players = home_team['player_id'].unique().tolist()
  45. for player_id in home_players:
  46. df = home_team.query('player_id == {id}'.format(id=player_id))
  47. df.plot(x='x', y='y', ax=ax, linewidth=4, legend=False)
  48. # visualize player movements for away team
  49. away_players = away_team['player_id'].unique().tolist()
  50. for player_id in away_players:
  51. df = away_team.query('player_id == {id}'.format(id=player_id))
  52. df.plot(x='x', y='y', ax=ax, linewidth=4, legend=False)
  53. # query play description and possession team and add them in the title
  54. sql = """SELECT gameid, playid, playdescription, possessionteam FROM play
  55. WHERE gameid = {game} AND playid = {play}""".format(game=game_id, play=play_id)
  56. play_info = pd.read_sql(sql, conn).to_dict('records')
  57. plt.title('Possession team: {team}\nPlay: {play}'.format(team=play_info[0]['possessionteam'],
  58. play=play_info[0]['playdescription']))
  59. # show chart
  60. plt.show()

Then, you can run the draw_play function like this to visualize pre-snap player positions:

  1. draw_play(game_id=2018112900,
  2. play_id=2826,
  3. movements=False)

You can also visualize player movement during the play if you set movements to True:

  1. draw_play(game_id=2018112900,
  2. play_id=2826,
  3. home_label='position',
  4. away_label='displayname',
  5. movements=True)

Conclusion

We hope that through this tutorial you have been able to see how data that does not appear to be time-series initially, is in fact time-series data after all. With TimescaleDB, analyzing time-series data can be easy (and fun!) when you use hyperfunctions and continuous aggregates. We encourage you to try these functions in your own database and try experimenting with different kinds of analysis.