{ "cells": [ { "attachments": {}, "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "# Soccer analysis example\n", "\n", "\n", "\n", "[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/movingpandas/movingpandas-examples/main?filepath=2-analysis-examples/soccer-game.ipynb)\n", "[![IPYNB](https://img.shields.io/badge/view-ipynb-hotpink)](https://github.com/movingpandas/movingpandas-examples/blob/main/2-analysis-examples/soccer-game.ipynb)\n", "[![HTML](https://img.shields.io/badge/view-html-green)](https://movingpandas.github.io/movingpandas-website/2-analysis-examples/soccer-game.html)\n", "\n", "This tutorial uses data extracted from video footage of a soccer game that was published in https://github.com/Friends-of-Tracking-Data-FoTD/Last-Row\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import pandas as pd\n", "import geopandas as gpd\n", "import movingpandas as mpd\n", "import shapely as shp\n", "import holoviews as hv\n", "import hvplot.pandas \n", "import matplotlib.pyplot as plt\n", "\n", "from geopandas import GeoDataFrame, read_file\n", "from shapely.geometry import Point, LineString, Polygon\n", "from datetime import datetime, timedelta\n", "from holoviews import opts, dim\n", "from os.path import exists\n", "from urllib.request import urlretrieve\n", "\n", "import warnings\n", "warnings.filterwarnings('ignore')\n", "\n", "hvplot_defaults = {'line_width':5, 'frame_height':350, 'frame_width':700, 'colorbar':True, 'tiles':None, 'geo':False,}\n", "\n", "mpd.show_versions()" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Loading soccer dataset from Github\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def get_file_from_url(url):\n", " file = url.split('/')[-1]\n", " if not exists(file):\n", " urlretrieve(url, file)\n", " return file \n", "\n", "def get_df_from_gh_url(url):\n", " file = get_file_from_url(url)\n", " return pd.read_csv(file)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "input_file = \"https://raw.githubusercontent.com/Friends-of-Tracking-Data-FoTD/Last-Row/master/datasets/positional_data/liverpool_2019.csv\"\n", "df = get_df_from_gh_url(input_file)\n", "df.drop(columns=['Unnamed: 0'], inplace=True)\n", "print(f'Number of records: {len(df)}')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df.head()" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "From the metadata: \n", "\n", "> * play: the scoreline after the goal. The team who scored the goal is the one next to the brackets.\n", "> * frame: the frame number for the current location. Data provided has 20 frames per second.\n", "> * player: the id of the player. The id is consistent within a play but not between plays.\n", "> * player_num: the player jersey number. This number is the official one, and did not change for Liverpool in 2019. You can check the corresponding names at this wikipedia link.\n", "> * x, y: coordinates for the player/ball. Pitch coordinates go from 0 to 100 on each axis.\n", "> * dx, dx: change in (x,y) coordinates from last frame to current frame\n", "> * z: height, from 0 to 1.5 (only filled for the ball)\n", "> * bgcolor: the main color for the team (used as background color)\n", "> * edgecolor the secondary color (used as edge color)\n", "\n", "And accoring to https://en.wikipedia.org/wiki/Football_pitch \n", "\n", "> the preferred size for many professional teams' stadiums is 105 by 68 metres" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plays = list(df.play.unique())\n", "\n", "def to_timestamp(row):\n", " # plays to date\n", " day = plays.index(row.play)+1\n", " start_time = datetime(2019,1,day,12,0,0)\n", " # frames to time\n", " td = timedelta(milliseconds=1000/20*row.frame)\n", " return start_time + td\n", "\n", "# frame: the frame number for the current location. Data provided has 20 frames per second\n", "df['time'] = df.apply(to_timestamp, axis=1)\n", "df.set_index('time', inplace=True)\n", "\n", "# the preferred size for many professional teams' stadiums is 105 by 68 metres, accoring to https://en.wikipedia.org/wiki/Football_pitch\n", "pitch_length = 105\n", "pitch_width = 68\n", "df.x = df.x / 100 * pitch_length \n", "df.y = df.y / 100 * pitch_width\n", "\n", "df" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df['team'].value_counts().plot(title='team', kind='bar', figsize=(15,3))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df['player_num'].value_counts().plot(title='player_num', kind='bar', figsize=(15,3))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df['team'] = df['team'].astype('category').cat.as_ordered()\n", "df['player'] = df['player'].astype('category').cat.as_ordered()\n", "df['player_num'] = df['player_num'].astype('category').cat.as_ordered()" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "Finally, let's create trajectories:" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Trajectories\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%time\n", "CRS = None\n", "tc = mpd.TrajectoryCollection(df, 'player', x='x', y='y', crs=CRS)\n", "mpd.TemporalSplitter(tc).split(mode=\"day\")\n", "print(f\"Finished creating {len(tc)} trajectories\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "pitch = Polygon([(0, 0), (0, pitch_width), (pitch_length, pitch_width), (pitch_length, 0), (0, 0)])\n", "plotted_pitch = GeoDataFrame(pd.DataFrame([{'geometry': pitch, 'id': 1}]), crs=CRS).hvplot(color='white', alpha=0.5)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plotted_pitch * tc.filter('player_num', 20).hvplot(**hvplot_defaults)" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Plays" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "PLAY = 2\n", "title = f'Play {PLAY} {plays[PLAY]}'\n", "play_trajs = tc.filter('play', plays[PLAY])\n", "play_trajs" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "play_trajs.plot(column='team', colormap={'attack':'hotpink', 'defense':'turquoise'})" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "generalized = mpd.MinTimeDeltaGeneralizer(play_trajs).generalize(tolerance=timedelta(seconds=0.5))" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "generalized.add_speed()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "generalized.hvplot(title=title, c='speed', hover_cols=['player', 'team'], **hvplot_defaults)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "(\n", " plotted_pitch * \n", " generalized.hvplot(title=title, c='speed', hover_cols=['player'], cmap='Viridis', **hvplot_defaults)\n", ")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "get_file_from_url('https://github.com/movingpandas/movingpandas/raw/main/tutorials/data/soccer_field.png')\n", "\n", "pitch_img = hv.RGB.load_image('soccer_field.png', bounds=(0,0,pitch_length,pitch_width)) \n", "(\n", " pitch_img * \n", " generalized.hvplot(title=title, c='team', colormap={'attack':'limegreen', 'defense':'purple'}, hover_cols=['team'], **hvplot_defaults) * \n", " generalized.get_start_locations().hvplot(label='start', color='orange')\n", ")" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "(\n", " pitch_img * \n", " generalized.hvplot(title=title, c='team', hover_cols=['team'], **hvplot_defaults) * \n", " generalized.get_start_locations().hvplot(label='start', c='team', hover_cols=['team'], colormap={'attack':'limegreen', 'defense':'purple'}, colorbar=True, legend=True)\n", ")" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Continue exploring MovingPandas\n", "\n", "1. [Bird migration analysis](bird-migration.ipynb)\n", "1. [Ship data analysis](ship-data.ipynb)\n", "1. [Horse collar data exploration](horse-collar.ipynb)\n", "1. [OSM traces](osm-traces.ipynb)\n", "1. [Soccer game](soccer-game.ipynb)\n", "1. [Mars rover & heli](mars-rover.ipynb)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.10" } }, "nbformat": 4, "nbformat_minor": 4 }