from pathlib import Path
from typing import Dict, Tuple, Union
import warnings
from lxml import etree
import iso8601
import numpy as np
import pandas as pd
from floodlight.core.code import Code
from floodlight.core.events import Events
from floodlight.core.pitch import Pitch
from floodlight.core.xy import XY
from floodlight.core.teamsheet import Teamsheet
from floodlight.io.utils import get_and_convert
def _create_periods_from_dat(
filepath_positions: Union[str, Path]
) -> Tuple[Dict[str, Tuple[int, int]], int]:
"""Parses over position file and returns dictionary with periods as well as an
estimate of the framerate based on the timedelta between multiple frames.
Parameters
----------
filepath_positions: str or pathlib.Path
Path to XML File where the Position data in DFL format is saved.
Returns
-------
periods: Dict[str, Tuple[int, int]
Dictionary with times for the segment:
``periods[segment] = (starttime, endtime)``
est_framerate: int
Estimated temporal resolution of data in frames per second/Hertz.
"""
periods = {}
framerate_est = None
# retrieve information from ball frame sets
for _, frame_set in etree.iterparse(filepath_positions, tag="FrameSet"):
if frame_set.get("TeamId").lower() == "ball":
frames = [frame for frame in frame_set.iterfind("Frame")]
periods[frame_set.get("GameSection")] = (
int(frames[0].get("N")),
int(frames[-1].get("N")),
)
delta = iso8601.parse_date(frames[1].get("T")) - iso8601.parse_date(
frames[0].get("T")
)
if framerate_est is None:
framerate_est = int(round(1 / delta.total_seconds()))
elif framerate_est != int(round(1 / delta.total_seconds())):
warnings.warn(
f"Framerate estimation yielded diverging results."
f"The originally estimated framerate of {framerate_est} Hz did not "
f"match the current estimation of "
f"{int(round(1 / delta.total_seconds()))} Hz. This might be "
f"caused by missing frame(s) in the position data."
f"Continuing by choosing the latest estimation of "
f"{int(round(1 / delta.total_seconds()))} Hz"
)
framerate_est = int(round(1 / delta.total_seconds()))
frame_set.clear()
return periods, framerate_est
def _get_event_description(
elem: etree.Element,
) -> Tuple[str, Dict[str, Union[str, int]]]:
"""Returns the full description of a single XML Event in the DFL format.
Parameters
----------
elem: lxml.etree.Element
lxml.etree.Element with the Event information.
Returns
-------
eID: str
High-level description for the current event.
attrib: Dict
Additional attributes for the current event in the form
``attrib[category] = label``.
"""
# read description
eID = elem.tag
# read additional attributes
attrib = {}
for category in elem.attrib:
attrib[category] = elem.attrib[category]
# find nested elements
nested_eID = None
nested_attrib = None
if elem.find("Play") is not None:
nested_eID, nested_attrib = _get_event_description(elem.find("Play"))
elif elem.find("Pass") is not None:
nested_eID, nested_attrib = _get_event_description(elem.find("Pass"))
elif elem.find("Cross") is not None:
nested_eID, nested_attrib = _get_event_description(elem.find("Cross"))
elif elem.find("ShotAtGoal") is not None:
nested_eID, nested_attrib = _get_event_description(elem.find("ShotAtGoal"))
nested_attrib.update(attrib)
elif elem.find("SuccessfulShot") is not None:
nested_eID, nested_attrib = _get_event_description(elem.find("SuccessfulShot"))
nested_attrib.update(attrib)
elif elem.find("SavedShot") is not None:
nested_eID, nested_attrib = _get_event_description(elem.find("SavedShot"))
nested_attrib.update(attrib)
elif elem.find("BlockedShot") is not None:
nested_eID, nested_attrib = _get_event_description(elem.find("BlockedShot"))
nested_attrib.update(attrib)
elif elem.find("ShotWide") is not None:
nested_eID, nested_attrib = _get_event_description(elem.find("ShotWide"))
nested_attrib.update(attrib)
elif elem.find("ShotWoodWork") is not None:
nested_eID, nested_attrib = _get_event_description(elem.find("ShotWoodWork"))
nested_attrib.update(attrib)
elif elem.find("OtherShot") is not None:
nested_eID, nested_attrib = _get_event_description(elem.find("OtherShot"))
# update nested elements
if nested_eID is not None and nested_attrib is not None:
eID += "_" + nested_eID
attrib.update(nested_attrib)
return eID, attrib
def _get_event_outcome(eID, attrib) -> int:
"""Returns the outcome of a single Event in the DFL format.
Parameters
----------
eID: str
High-level description for the current event.
attrib: Dict
Additional attributes for the current event in the form
``attrib[category] = label``.
Returns
-------
outcome: int
Outcome coded as 1 (success) or 0 (failure) of the current event or np.nan in
case no outcome is defined.
"""
outcome = np.nan
# well-defined outcome
if "TacklingGame" in eID:
if attrib["WinnerRole"] == "withoutBallControl":
outcome = 1
elif attrib["WinnerRole"] == "withBallControl":
outcome = 0
elif "BallClaiming" in eID:
if "Type" in attrib:
if attrib["Type"] in ["BallClaimed"]:
outcome = 1
elif attrib["Type"] in ["BallHeld"]:
outcome = 0
elif "Play" in eID:
if "Successful" in attrib:
if attrib["Successful"] == "true":
outcome = 1
elif attrib["Successful"] == "false":
outcome = 0
elif "ShotAtGoal" in eID:
if "SuccessfulShot" in eID:
outcome = 1
else:
outcome = 0
# per definition no outcome
elif eID in [
"OwnGoal",
"PreventedOwnGoal",
"DefensiveClearance",
"Foul",
"Offside",
"Caution",
"SendingOff",
"Substitution",
"KickoffWhistle",
"FinalWhistle",
"FairPlay",
"RefereeBall",
"OtherBallAction",
"OtherPlayerAction",
]:
pass
# missing sub child in event
elif eID in [
"FreeKick",
"ThrowIn",
"CornerKick",
"Penalty",
"GoalKick",
"Kickoff",
]:
pass
return outcome
def _get_event_team_and_player(eID, attrib) -> Tuple[str, str]:
"""Returns the player and team of a single Event in the DFL format.
Parameters
----------
eID: str
High-level description for the current event.
attrib: Dict
Additional attributes for the current event in the form
``attrib[category] = label``.
Returns
-------
team: str
DFL Team ID of the Event
player: str
DFL Player ID of the Event
"""
# team
team = None
if "Team" in attrib:
team = attrib["Team"]
elif eID == "TacklingGame" and "WinnerTeam" in attrib and "LoserTeam" in attrib:
if attrib["WinnerRole"] == "withBallControl":
team = attrib["WinnerTeam"]
elif attrib["WinnerRole"] == "withoutBallControl":
team = attrib["LoserTeam"]
elif "TeamFouler" in attrib:
team = attrib["TeamFouler"]
# player
player = None
if "Player" in attrib:
player = attrib["Player"]
elif eID == "TacklingGame" and "Winner" in attrib and "Loser" in attrib:
if attrib["WinnerRole"] == "withBallControl":
player = attrib["Winner"]
elif attrib["WinnerRole"] == "withoutBallControl":
player = attrib["Loser"]
elif "Fouler" in attrib:
player = attrib["Fouler"]
return team, player
[docs]def read_pitch_from_mat_info_xml(filepath_mat_info: Union[str, Path]) -> Pitch:
"""Reads match_information XML file and returns the playing Pitch.
Parameters
----------
filepath_mat_info: str or pathlib.Path
Full path to XML File where the Match Information data in DFL format is saved.
Returns
-------
pitch: Pitch
Pitch object with actual pitch length and width.
"""
# set up XML tree
tree = etree.parse(str(filepath_mat_info))
root = tree.getroot()
# parse pitch
length = root.find("MatchInformation").find("Environment").get("PitchX")
length = float(length) if length else None
width = root.find("MatchInformation").find("Environment").get("PitchY")
width = float(width) if width else None
pitch = Pitch.from_template(
"dfl",
length=length,
width=width,
sport="football",
)
return pitch
[docs]def read_teamsheets_from_mat_info_xml(filepath_mat_info) -> Dict[str, Teamsheet]:
"""Reads match_information XML file and returns two teamsheet objects for the home
and the away team.
Parameters
----------
filepath_mat_info: str or pathlib.Path
Full path to XML File where the Match Information data in DFL format is saved.
Returns
-------
teamsheets: Dict[str, Teamsheet]
Dictionary with teamsheets for the home team and the away team.
"""
# set up XML tree
tree = etree.parse(str(filepath_mat_info))
root = tree.getroot()
# initialize teamsheets
teamsheets = {
"Home": pd.DataFrame(
columns=["player", "position", "team", "jID", "pID", "tID"]
),
"Away": pd.DataFrame(
columns=["player", "position", "team", "jID", "pID", "tID"]
),
}
# find team ids
team_informations = root.find("MatchInformation").find("Teams")
home_id = root.find("MatchInformation").find("General").get("HomeTeamId")
if "AwayTeamId" in root.find("MatchInformation").find("General").attrib:
away_id = root.find("MatchInformation").find("General").get("AwayTeamId")
elif "GuestTeamId" in root.find("MatchInformation").find("General").attrib:
away_id = root.find("MatchInformation").find("General").get("GuestTeamId")
else:
away_id = None
# parse player information
for team_info in team_informations:
if team_info.get("TeamId") == home_id:
team = "Home"
elif team_info.get("TeamId") == away_id:
team = "Away"
else:
team = None
# skip referees sometimes referred to as a team in new data formats
if team not in ["Home", "Away"]:
continue
# create list of players
players = team_info.find("Players")
# create teamsheets
teamsheets[team]["player"] = [
get_and_convert(player, "Shortname", str) for player in players
]
teamsheets[team]["pID"] = [
get_and_convert(player, "PersonId", str) for player in players
]
teamsheets[team]["jID"] = [
get_and_convert(player, "ShirtNumber", int) for player in players
]
teamsheets[team]["position"] = [
get_and_convert(player, "PlayingPosition", str) for player in players
]
teamsheets[team]["tID"] = team_info.get("TeamId")
teamsheets[team]["team"] = team_info.get("TeamName")
# create teamsheet objects
for team in teamsheets:
teamsheets[team] = Teamsheet(teamsheets[team])
return teamsheets
[docs]def read_event_data_xml(
filepath_events: Union[str, Path],
filepath_mat_info: Union[str, Path],
teamsheet_home: Teamsheet = None,
teamsheet_away: Teamsheet = None,
) -> Tuple[Dict[str, Dict[str, Events]], Dict[str, Teamsheet], Pitch]:
"""Parses a DFL Match Event XML file and extracts the event data as well as
teamsheets.
The structure of the official tracking system of the DFL (German Football League)
contains two separate xml files, one containing the actual data as well as a
metadata file containing information about teams, pitch size, and start- and
endframes of match periods. This function provides high-level access to DFL data by
parsing "the full match" and returning Events-objects parsed from the event data
xml-file as well as Teamsheet-objects parsed from the metadata xml-file. The number
of segments is inferred from the data, yet data for each segment is stored in a
separate object.
Parameters
----------
filepath_events: str or pathlib.Path
Full path to XML File where the Event data in DFL format is saved.
filepath_mat_info: str or pathlib.Path
Full path to XML File where the Match Information data in DFL format is saved.
teamsheet_home: Teamsheet, optional
Teamsheet-object for the home team used to assign the tIDs of the teams to the
"Home" and "Away" position. If given as None (default), teamsheet is extracted
from the Match Information XML file.
teamsheet_away: Teamsheet, optional
Teamsheet-object for the away team. If given as None (default), teamsheet is
extracted from the Match Information XML file. See teamsheet_home for details.
Returns
-------
data_objects: Tuple[Dict[str, Dict[str, Events]], Dict[str, Teamsheet], Pitch]
Tuple of (nested) floodlight core objects with shape (events_objects,
teamsheets, pitch).
``events_objects`` is a nested dictionary containing ``Events`` objects for
each team and segment of the form ``events_objects[segment][team] = Events``.
For a typical league match with two halves and teams this dictionary looks like:
``{
'firstHalf': {'Home': Events, 'Away': Events},
'secondHalf': {'Home': Events,'Away': Events}
}``.
``teamsheets`` is a dictionary containing ``Teamsheet`` objects for each team
of the form ``teamsheets[team] = Teamsheet``.
``pitch`` is a ``Pitch`` object corresponding to the data.
Notes
-----
The DFL format of handling event data information involves an elaborate use of
certain event attributes, which attach additional information to certain events.
There also exist detailed definitions for these attributes. Parsing this information
involves quite a bit of logic and is planned to be included in further releases. As
of now, qualifier information is parsed as a string in the `qualifier` column of the
returned DataFrame and might be transformed to a dict of the form:
`{attribute: value}`.
"""
# set up XML tree
tree = etree.parse(str(filepath_events))
root = tree.getroot()
# read metadata
pitch = read_pitch_from_mat_info_xml(filepath_mat_info)
# create or check teamsheet objects
if teamsheet_home is None and teamsheet_away is None:
teamsheets = read_teamsheets_from_mat_info_xml(filepath_mat_info)
teamsheet_home = teamsheets["Home"]
teamsheet_away = teamsheets["Away"]
elif teamsheet_home is None:
teamsheets = read_teamsheets_from_mat_info_xml(filepath_mat_info)
teamsheet_home = teamsheets["Home"]
elif teamsheet_away is None:
teamsheets = read_teamsheets_from_mat_info_xml(filepath_mat_info)
teamsheet_away = teamsheets["Away"]
else:
pass
# potential check
# find start of halves
start_times = {}
start_events = root.findall("Event/KickoffWhistle")
# look at different encodings as the data format changed over time
if not bool(start_events): # if no KickoffWhistle is in data search for Kickoff
start_events = root.findall("Event/Kickoff")
if not bool(start_events): # if no Kickoff is in data search for KickOff
start_events = root.findall("Event/KickOff")
for event in start_events:
if event.get("GameSection") is not None:
start_times[event.get("GameSection")] = iso8601.parse_date(
event.getparent().get("EventTime")
)
# find end of halves
end_times = {}
end_events = root.findall("Event/FinalWhistle")
for event in end_events:
if event.get("GameSection") is not None:
end_times[event.get("GameSection")] = iso8601.parse_date(
event.getparent().get("EventTime")
)
# initialize periods
segments = list(start_times.keys())
periods = {}
for segment in segments:
periods[segment] = (start_times[segment], end_times[segment])
# set up bins
team_events = {segment: {} for segment in segments}
# loop over events
for elem in root.findall("Event"):
# initialize
event = {}
# check for structure that is an element Event with a single child
if len(elem) != 1:
warnings.warn(
"An XML Event has multiple children. This likely causes imprecise "
"Event descriptions and outcomes."
)
# absolute time information (timestamp)
event["timestamp"] = iso8601.parse_date(elem.get("EventTime"))
event["gameclock"] = np.nan
# segment in which event took place
segment = None
for seg in segments:
if periods[seg][0] <= event["timestamp"] <= periods[seg][1]:
segment = seg
# assign to closest start point if not within any segments
if segment is None:
seg_ind = np.argmin(
[np.abs(event["timestamp"] - periods[seg][0]) for seg in segments]
)
segment = segments[int(seg_ind)]
# relative time information (gameclock)
event["gameclock"] = (event["timestamp"] - periods[segment][0]).total_seconds()
event["minute"] = np.floor(event["gameclock"] / 60)
event["second"] = np.floor(event["gameclock"] - event["minute"] * 60)
# description, outcome, team, and player
child = next(iter(elem))
eID, attrib = _get_event_description(child)
outcome = _get_event_outcome(eID, attrib)
tID, pID = _get_event_team_and_player(eID, attrib)
event["eID"] = eID
event["qualifier"] = attrib
event["outcome"] = outcome
event["tID"] = tID
event["pID"] = pID
# insert to bin
if tID not in team_events[segment]:
team_events[segment][tID] = []
if event["eID"] == "Substitution": # split for the special case substitution
# in-sub
event["eID"] = "InSubstitution"
event["pID"] = event["qualifier"]["PlayerIn"]
team_events[segment][tID].append(event)
# out-sub
event["eID"] = "OutSubstitution"
event["pID"] = event["qualifier"]["PlayerOut"]
team_events[segment][tID].append(event)
else:
team_events[segment][tID].append(event)
# postprocessing
team_dfs = {segment: {} for segment in segments}
for segment in segments:
# teams
teams = [tID for tID in team_events[segment] if tID is not None]
# loop over teams
for tID in teams:
# assign events with tID None to both teams
team_events[segment][tID] += team_events[segment][None]
# transform to data DataFrame
team_dfs[segment][tID] = pd.DataFrame(team_events[segment][tID])
# columns to standard order
team_dfs[segment][tID] = team_dfs[segment][tID][
[
"eID",
"gameclock",
"tID",
"pID",
"outcome",
"timestamp",
"minute",
"second",
"qualifier",
]
]
team_dfs[segment][tID] = team_dfs[segment][tID].sort_values("gameclock")
team_dfs[segment][tID] = team_dfs[segment][tID].reset_index(drop=True)
# check for teams
team1 = list(team_dfs[segments[0]].keys())[0]
team2 = list(team_dfs[segments[0]].keys())[1]
if not np.all([team1 in team_dfs[segment].keys() for segment in segments]):
KeyError(
f"Found tID {team1} of the first segment missing in at least one "
f"other segment!"
)
if not np.all([team2 in team_dfs[segment].keys() for segment in segments]):
KeyError(
f"Found tID {team2} of the first segment missing in at least one "
f"other segment!"
)
# link team1 and team2 to home and away
home_tID = teamsheet_home.teamsheet.at[0, "tID"]
away_tID = teamsheet_away.teamsheet.at[0, "tID"]
links_team_to_role = {
"Home": home_tID,
"Away": away_tID,
}
# check if home and away tIDs occur in event data
if team1 != home_tID and team2 != home_tID:
raise AttributeError(
f"Neither tID of teams in the event data ({team1} and {team2}) "
f"matches the tID of the home team from the "
f"teamsheet_home ({home_tID})!"
)
if team1 != away_tID and team2 != away_tID:
raise AttributeError(
f"Neither tID of teams in the event data ({team1} and {team2}) "
f"matches the tID of the away team from the "
f"teamsheet_away ({away_tID})!"
)
# create objects
events_objects = {}
for segment in segments:
events_objects[segment] = {}
for team in ["Home", "Away"]:
events_objects[segment][team] = Events(
events=team_dfs[segment][links_team_to_role[team]],
)
teamsheets = {
"Home": teamsheet_home,
"Away": teamsheet_away,
}
# pack objects
data_objects = (events_objects, teamsheets, pitch)
return data_objects
[docs]def read_position_data_xml(
filepath_positions: Union[str, Path],
filepath_mat_info: Union[str, Path],
teamsheet_home: Teamsheet = None,
teamsheet_away: Teamsheet = None,
) -> Tuple[
Dict[str, Dict[str, XY]],
Dict[str, Code],
Dict[str, Code],
Dict[str, Teamsheet],
Pitch,
]:
"""Parse DFL files and extract position data, possession and ballstatus codes as
well as pitch information and teamsheets.
The structure of the official tracking system of the DFL (German Football League)
contains two separate xml files, one containing the actual data as well as a
metadata file containing information about teams, pitch size, and start- and
endframes of match periods. However, since no information about framerate is
contained in the metadata, the framerate is estimated from the time difference
between individual frames. This function provides high-level access to DFL data by
parsing "the full match" and returning XY- and Code-objects parsed from the position
data xml-file as well as Pitch- and Teamsheet-objects parsed from the metadata
xml-file.
Parameters
----------
filepath_positions: str or pathlib.Path
Full path to XML File where the Position data in DFL format is saved.
filepath_mat_info: str or pathlib.Path
Full path to XML File where the Match Information data in DFL format is saved.
teamsheet_home: Teamsheet, optional
Teamsheet-object for the home team used to create link dictionaries of the form
`links[team][jID] = xID` and `links[team][pID] = jID`. The links are used to
map players to a specific xID in the respective XY objects. Should be supplied
for custom ordering. If given as None (default), teamsheet is extracted from the
Match Information XML file and its xIDs are assigned in order of appearance.
teamsheet_away: Teamsheet, optional
Teamsheet-object for the away team. If given as None (default), teamsheet is
extracted from the Match Information XML file. See teamsheet_home for details.
Returns
-------
data_objects: Tuple[Dict[str, Dict[str, XY]], Dict[str, Code], Dict[str, Code], \
Dict[str, Teamsheet], Pitch]
Tuple of (nested) floodlight core objects with shape (xy_objects,
possession_objects, ballstatus_objects, teamsheets, pitch).
``xy_objects`` is a nested dictionary containing ``XY`` objects for each team
and segment of the form ``xy_objects[segment][team] = XY``. For a typical
league match with two halves and teams this dictionary looks like:
``{'firstHalf': {'Home': XY, 'Away': XY}, 'secondHalf': {'Home': XY, 'Away':
XY}}``.
``possession_objects`` is a dictionary containing ``Code`` objects with
possession information (home or away) for each segment of the form
``possession_objects[segment] = Code``.
``ballstatus_objects`` is a dictionary containing ``Code`` objects with
ballstatus information (dead or alive) for each segment of the form
``ballstatus_objects[segment] = Code``.
``teamsheets`` is a dictionary containing ``Teamsheet`` objects for each team
of the form ``teamsheets[team] = Teamsheet``.
``pitch`` is a ``Pitch`` object corresponding to the data.
"""
# read metadata
pitch = read_pitch_from_mat_info_xml(filepath_mat_info)
# create or check teamsheet objects
if teamsheet_home is None and teamsheet_away is None:
teamsheets = read_teamsheets_from_mat_info_xml(filepath_mat_info)
teamsheet_home = teamsheets["Home"]
teamsheet_away = teamsheets["Away"]
elif teamsheet_home is None:
teamsheets = read_teamsheets_from_mat_info_xml(filepath_mat_info)
teamsheet_home = teamsheets["Home"]
elif teamsheet_away is None:
teamsheets = read_teamsheets_from_mat_info_xml(filepath_mat_info)
teamsheet_away = teamsheets["Away"]
else:
pass
# potential check
# create links
if "xID" not in teamsheet_home.teamsheet.columns:
teamsheet_home.add_xIDs()
if "xID" not in teamsheet_away.teamsheet.columns:
teamsheet_away.add_xIDs()
links_jID_to_xID = {
"Home": teamsheet_home.get_links("jID", "xID"),
"Away": teamsheet_away.get_links("jID", "xID"),
}
links_pID_to_jID = {
"Home": teamsheet_home.get_links("pID", "jID"),
"Away": teamsheet_away.get_links("pID", "jID"),
}
# create periods
periods, framerate_est = _create_periods_from_dat(filepath_positions)
segments = list(periods.keys())
# infer data array shapes
number_of_home_players = max(links_jID_to_xID["Home"].values()) + 1
number_of_away_players = max(links_jID_to_xID["Away"].values()) + 1
number_of_frames = {}
for segment in segments:
start = periods[segment][0]
end = periods[segment][1]
number_of_frames[segment] = end - start + 1
# bins
xydata = {
"Home": {
segment: np.full(
[number_of_frames[segment], number_of_home_players * 2], np.nan
)
for segment in segments
},
"Away": {
segment: np.full(
[number_of_frames[segment], number_of_away_players * 2], np.nan
)
for segment in segments
},
"Ball": {
segment: np.full([number_of_frames[segment], 2], np.nan)
for segment in segments
},
}
codes = {
code: {segment: [] for segment in segments}
for code in ["possession", "ballstatus"]
}
# loop over frame sets containing player & ball positions for all segments
for _, frame_set in etree.iterparse(filepath_positions, tag="FrameSet"):
# ball
if frame_set.get("TeamId").lower() == "ball":
# (x, y) position
segment = frame_set.get("GameSection")
xydata["Ball"][segment][:, 0] = np.array(
[float(frame.get("X")) for frame in frame_set.iterfind("Frame")]
)
xydata["Ball"][segment][:, 1] = np.array(
[float(frame.get("Y")) for frame in frame_set.iterfind("Frame")]
)
# codes
codes["ballstatus"][segment] = [
float(frame.get("BallStatus")) for frame in frame_set.iterfind("Frame")
]
codes["possession"][segment] = [
float(frame.get("BallPossession"))
for frame in frame_set.iterfind("Frame")
]
# teams
else:
# find identity of frame set
frames = [frame for frame in frame_set.iterfind("Frame")]
segment = frame_set.get("GameSection")
if frame_set.get("PersonId") in links_pID_to_jID["Home"]:
team = "Home"
jrsy = links_pID_to_jID[team][frame_set.get("PersonId")]
elif frame_set.get("PersonId") in links_pID_to_jID["Away"]:
team = "Away"
jrsy = links_pID_to_jID[team][frame_set.get("PersonId")]
else:
continue
# possible error or warning
# insert (x,y) data to correct place in bin
start = int(frames[0].get("N")) - periods[segment][0]
end = int(frames[-1].get("N")) - periods[segment][0] + 1
x_col = (links_jID_to_xID[team][jrsy]) * 2
y_col = (links_jID_to_xID[team][jrsy]) * 2 + 1
xydata[team][segment][start:end, x_col] = np.array(
[float(frame.get("X")) for frame in frames]
)
xydata[team][segment][start:end, y_col] = np.array(
[float(frame.get("Y")) for frame in frames]
)
frame_set.clear()
# create objects
xy_objects = {}
possession_objects = {}
ballstatus_objects = {}
for segment in segments:
xy_objects[segment] = {}
possession_objects[segment] = Code(
code=np.array(codes["possession"][segment]),
name="possession",
definitions={1: "Home", 2: "Away"},
framerate=framerate_est,
)
ballstatus_objects[segment] = Code(
code=np.array(codes["ballstatus"][segment]),
name="ballstatus",
definitions={0: "Dead", 1: "Alive"},
framerate=framerate_est,
)
for team in ["Home", "Away", "Ball"]:
xy_objects[segment][team] = XY(
xy=xydata[team][segment],
framerate=framerate_est,
)
teamsheets = {
"Home": teamsheet_home,
"Away": teamsheet_away,
}
# pack objects
data_objects = (
xy_objects,
possession_objects,
ballstatus_objects,
teamsheets,
pitch,
)
return data_objects