Skip to content

Commit

Permalink
Adding tracking data improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
jpatacz committed May 11, 2022
1 parent 8c568c9 commit 054d1b8
Show file tree
Hide file tree
Showing 9 changed files with 754 additions and 65 deletions.
38 changes: 38 additions & 0 deletions .github/workflows/packaging-and-releasing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,22 @@ jobs:
- name: Edit version
run: sed -r -i 's/(.*)(version=.*)([0-9]+).([0-9]+).([0-9]+)(.*)/echo "\1\2\3.\4.$((\5+1))\6"/ge' setup.py

- name: Branch protection OFF
uses: octokit/[email protected]
with:
route: PUT /repos/:repository/branches/1.0.0/protection
repository: ${{ github.repository }}
required_status_checks: |
null
enforce_admins: |
null
required_pull_request_reviews: |
null
restrictions: |
null
env:
GITHUB_TOKEN: ${{ secrets.GH_ACTIONS_REPO_ADMIN_TOKEN }}

- name: Commit changes
uses: EndBug/add-and-commit@v7
with:
Expand All @@ -29,6 +45,28 @@ jobs:
with:
node-version: '14'

- name: Branch protection ON
uses: octokit/[email protected]
with:
route: PUT /repos/:repository/branches/1.0.0/protection
repository: ${{ github.repository }}
mediaType: |
previews:
- luke-cage
required_status_checks: |
strict: true
contexts:
- build
enforce_admins: |
null
required_pull_request_reviews: |
dismiss_stale_reviews: true
required_approving_review_count: 1
restrictions: |
null
env:
GITHUB_TOKEN: ${{ secrets.GH_ACTIONS_REPO_ADMIN_TOKEN }}

- name: Display setup.py
run: cat setup.py

Expand Down
5 changes: 5 additions & 0 deletions docks/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
requests>=2.20.0
makefun>=1.10.0
numpy>=1.18.4, <1.19.0
pandas
matplotlib>=3.5.0
8 changes: 6 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@
python_requires='>=3.6, <=3.9',
install_requires=[
'requests>=2.20.0',
'makefun>=1.10.0'
'makefun>=1.10.0',
'numpy>=1.18.4, <1.19.0',
'pandas',
'matplotlib>=3.5.0',
],
extras_require={
'test': 'mock==4.0.3',
'release': ['Sphinx==4.2.0', 'sphinx-rtd-theme==1.0.0']
'release': ['Sphinx==4.2.0', 'sphinx-rtd-theme==1.0.0'],
'physical_visualisation': 'pandasgui',
}
)
3 changes: 3 additions & 0 deletions skillcorner/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from pkg_resources import get_distribution

__version__ = get_distribution('skillcorner').version
329 changes: 267 additions & 62 deletions skillcorner/client.py

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions skillcorner/physical_visualisation_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from pandasgui import show
from skillcorner.client import SkillcornerClient


skc_client = SkillcornerClient(username='PUT_YOUR_LOGIN_HERE', password='PUT_YOUR_PASSWORD_HERE')
physical_data = skc_client.get_physical(params={'season': 6, 'competition': 1})
show(physical_data)
3 changes: 2 additions & 1 deletion skillcorner/tests/mocks/client_mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ def __init__(self, *args, **kwargs):
super(MockSkillcornerClient, self).__init__(*args, **kwargs)
logger.debug(f'Creating Skillcorner mock client instance')

def _skillcorner_request(self, url, method, params, paginated_request, timeout, pagination_limit=300):
def _skillcorner_request(self, url, method, params, paginated_request, timeout, output_format=None,
visualisation=None, json_data=None, pagination_limit=300, **kwargs):
"""
Mocked skillcorner_request method returning fake json response read from file.
Expand Down
154 changes: 154 additions & 0 deletions skillcorner/tracking_visualisation_example.ipynb

Large diffs are not rendered by default.

272 changes: 272 additions & 0 deletions skillcorner/visualisation_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
import json


def _get_json_list(response):
data_bytes = response.content
data_list = data_bytes.decode('utf-8').split("\n")
data = []
for line in data_list:
if line:
data.append(json.loads(line))
return data


def _convert_response_to_df(data, df_parameterization):
import pandas as pd
df = pd.DataFrame(data)

if not df_parameterization:
return df

elif df_parameterization=='positions_per_frame':
arrays = [[], []]

for frame in df.frame:
if df.data[frame]:
for i in range(len(df.data[frame])):
trackable_object = df.data[frame][i]['trackable_object']
if trackable_object not in arrays[0]:
arrays[0] += [trackable_object]*2
arrays[1] += ['x', 'y']

positions = _preprepare_positions(len(df.frame), len(arrays[0]))

for frame in df.frame:
if df.data[frame]:
for i in range(len(df.data[frame])):
trackable_object = df.data[frame][i]['trackable_object']
index = arrays[0].index(trackable_object)
positions[frame][index] = df.data[frame][i]['x']
positions[frame][index+1] = df.data[frame][i]['y']

names = ['trackable_object', 'position']
arrays_2 = [[], []]
arrays_2[0] = df.frame
arrays_2[1] = df.timestamp
tuples_2 = list(zip(*arrays_2))
names_2 = ['frame', 'timestamp']
index = pd.MultiIndex.from_tuples(tuples_2, names=names_2)

tuples = list(zip(*arrays))
multi_index = pd.MultiIndex.from_tuples(tuples, names=names)
data = pd.DataFrame(positions, columns=multi_index, index=index)

return data


def _preprepare_positions(dim_1, dim_2):
positions = []
for i in range(dim_1):
temp = []
for j in range(dim_2):
temp.append(0)
positions.append(temp)
return positions


def create_visualisation_per_frame(self, match, frame):
"""
Function to plot extrapolated data visualisation at indicated frame.
:param match: df of extrapolated tracking data or int id of match which should be presented
:param frame: int indicating which frame visualisation to be presented
"""
import matplotlib.pyplot as plt
import pandas as pd

if not isinstance(match, pd.DataFrame):
tracking_data = self.get_match_tracking_data(match)
else:
tracking_data = match

x = tracking_data.loc[frame, tracking_data.columns.get_level_values(1)=="x"]
y = tracking_data.loc[frame, tracking_data.columns.get_level_values(1)=="y"]
trackable_objects = tracking_data.columns.droplevel(1).unique()
timestamp = tracking_data.index[frame][1]
x[x==0] = float('nan')
y[y==0] = float('nan')

fig, ax = plt.subplots()
fig.suptitle(f'Tracking visualisation of frame: {frame}')
fig.canvas.set_window_title('Skillcorner')
_plot_field(ax)
ax.get_xaxis().set_visible(False)
ax.get_yaxis().set_visible(False)
text = fig.text(0.15, 0.05, f"Timestamp: {timestamp}")
scatter = ax.scatter(x, y)
plt.show()


def create_full_visualisation(self, match, valinit=100):
"""
Function to plot extrapolated data visualisation for the full game. Uses matplot Slider widget.
:param match: df of extrapolated tracking data or int id of match which should be presented
:param valinit: int indicating frame of starting point for slider
"""
from matplotlib.widgets import Slider
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np

if not isinstance(match, pd.DataFrame):
tracking_data = self.get_match_tracking_data(match)
else:
tracking_data = match

x = tracking_data.loc[valinit, tracking_data.columns.get_level_values(1)=="x"]
y = tracking_data.loc[valinit, tracking_data.columns.get_level_values(1)=="y"]
x[x==0] = float('nan')
y[y==0] = float('nan')
timestamp = tracking_data.index[valinit][1]

fig, ax = plt.subplots()
fig.suptitle('Tracking visualisation')
fig.canvas.set_window_title('Skillcorner')
plt.subplots_adjust(bottom=0.25)
_plot_field(ax)
ax.get_xaxis().set_visible(False)
ax.get_yaxis().set_visible(False)
ax_slider = plt.axes([0.25, 0.1, 0.65, 0.03])
ax_slider.get_xaxis().set_visible(False)
ax_slider.get_yaxis().set_visible(False)
text = fig.text(0.15, 0.05, f"Timestamp: {timestamp}")
scatter = ax.scatter(x, y)
slider = Slider(ax=ax_slider, label='Frames', valmin=0, valmax=len(tracking_data)-1, valinit=valinit, valstep=1)

def update(frame):
x = tracking_data.loc[frame, tracking_data.columns.get_level_values(1)=="x"]
y = tracking_data.loc[frame, tracking_data.columns.get_level_values(1)=="y"]
x[x==0] = float('nan')
y[y==0] = float('nan')
timestamp = tracking_data.index[frame][1]
xx = np.vstack((x, y))
scatter.set_offsets(xx.T)
text.set_text(f"Timestamp: {timestamp}")

slider.on_changed(update)

plt.show()


def _plot_rectangle(x1, x2, y1, y2, ax):
ax.plot([x1, x1], [y1, y2], color="white", zorder=8000)
ax.plot([x2, x2], [y1, y2], color="white", zorder=8000)
ax.plot([x1, x2], [y1, y1], color="white", zorder=8000)
ax.plot([x1, x2], [y2, y2], color="white", zorder=8000)


def _plot_field(ax):
import matplotlib.pyplot as plt
from matplotlib.patches import Arc
# Pitch Outline & Centre Line
origin_x1 = -52.5
origin_x2 = 52.5
origin_y1 = -34
origin_y2 = 34

d = 2
rectangle = plt.Rectangle(
(origin_x1 - 2 * d, origin_y1 - 2 * d),
105 + 4 * d,
68 + 4 * d,
fc="green",
alpha=0.4,
zorder = -5000,
)
ax.add_patch(rectangle)
_plot_rectangle(origin_x1, origin_x2, origin_y1, origin_y2, ax)
ax.plot([0, 0], [origin_y1, origin_y2], color="white", zorder=8000)

# Left Penalty Area
penalty_box_length = 16.5
penalty_box_width = 40.3

x1 = origin_x1
x2 = origin_x1 + penalty_box_length
y1 = -penalty_box_width / 2
y2 = penalty_box_width / 2
_plot_rectangle(x1, x2, y1, y2, ax)

# Right Penalty Area
x1 = origin_x2 - penalty_box_length
x2 = origin_x2
y1 = -penalty_box_width / 2
y2 = penalty_box_width / 2
_plot_rectangle(x1, x2, y1, y2, ax)

# Left 6-yard Box
six_yard_box_length = 5.5
six_yard_box_width = 18.3

x1 = origin_x1
x2 = origin_x1 + six_yard_box_length
y1 = -six_yard_box_width / 2
y2 = six_yard_box_width / 2
_plot_rectangle(x1, x2, y1, y2, ax)

# Right 6-yard Box
x1 = origin_x2 - six_yard_box_length
x2 = origin_x2
y1 = -six_yard_box_width / 2
y2 = six_yard_box_width / 2
_plot_rectangle(x1, x2, y1, y2, ax)

# Left Goal
goal_width = 7.3
goal_length = 2

x1 = origin_x1 - goal_length
x2 = origin_x1
y1 = -goal_width / 2
y2 = goal_width / 2
_plot_rectangle(x1, x2, y1, y2, ax)

# Right Goal
x1 = origin_x2
x2 = origin_x2 + goal_length
y1 = -goal_width / 2
y2 = goal_width / 2
_plot_rectangle(x1, x2, y1, y2, ax)

# Prepare Circles
circle_radius = 9.15
penalty_spot_distance = 11
centreCircle = plt.Circle((0, 0), circle_radius, color="white", fill=False, zorder=8000)
centreSpot = plt.Circle((0, 0), 0.4, color="white", zorder=8000)
lx = origin_x1 + penalty_spot_distance
leftPenSpot = plt.Circle((lx, 0), 0.4, color="white", zorder=8000)
rx = origin_x2 - penalty_spot_distance
rightPenSpot = plt.Circle((rx, 0), 0.4, color="white", zorder=8000)

# Draw Circles
ax.add_patch(centreCircle)
ax.add_patch(centreSpot)
ax.add_patch(leftPenSpot)
ax.add_patch(rightPenSpot)

# Prepare Arcs
r = circle_radius * 2
leftArc = Arc(
(lx, 0),
height=r,
width=r,
angle=0,
theta1=307,
theta2=53,
color="white",
zorder=8000,
)
rightArc = Arc(
(rx, 0),
height=r,
width=r,
angle=0,
theta1=127,
theta2=233,
color="white",
zorder=8000,
)
# Draw Arcs
ax.add_patch(leftArc)
ax.add_patch(rightArc)

0 comments on commit 054d1b8

Please sign in to comment.