Skip to content

Commit

Permalink
feat(sportec): add referees to metadata; fix(sportec): parsing tracki…
Browse files Browse the repository at this point in the history
…ng data with referee

---------

Co-authored-by: UnravelSports [JB] <[email protected]>
Co-authored-by: Pieter Robberechts <[email protected]>
  • Loading branch information
3 people authored Dec 17, 2024
1 parent e359a99 commit 45ab84c
Show file tree
Hide file tree
Showing 6 changed files with 822 additions and 1 deletion.
43 changes: 42 additions & 1 deletion kloppy/domain/models/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

from .position import PositionType

from ...utils import deprecated
from ...utils import deprecated, snake_case

if sys.version_info >= (3, 8):
from typing import Literal
Expand Down Expand Up @@ -119,6 +119,46 @@ def __str__(self):
return self.value


class OfficialType(Enum):
"""Enumeration for types of officials (referees)."""

VideoAssistantReferee = "Video Assistant Referee"
MainReferee = "Main Referee"
AssistantReferee = "Assistant Referee"
FourthOfficial = "Fourth Official"

def __str__(self):
return self.value


@dataclass(frozen=True)
class Official:
"""
Represents an official (referee) with optional names and roles.
"""

official_id: str
name: Optional[str] = None
first_name: Optional[str] = None
last_name: Optional[str] = None
role: Optional[OfficialType] = None

@property
def full_name(self):
"""
Returns the full name of the official, falling back to role-based or ID-based naming.
"""
if self.name:
return self.name
if self.first_name and self.last_name:
return f"{self.first_name} {self.last_name}"
if self.last_name:
return self.last_name
if self.role:
return f"{snake_case(str(self.role))}_{self.official_id}"
return f"official_{self.official_id}"


@dataclass(frozen=True)
class Player:
"""
Expand Down Expand Up @@ -1016,6 +1056,7 @@ class Metadata:
game_id: Optional[str] = None
home_coach: Optional[str] = None
away_coach: Optional[str] = None
officials: Optional[List] = field(default_factory=list)
attributes: Optional[Dict] = field(default_factory=dict, compare=False)

def __post_init__(self):
Expand Down
38 changes: 38 additions & 0 deletions kloppy/infra/serializers/event/sportec/deserializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
CardType,
AttackingDirection,
PositionType,
Official,
OfficialType,
)
from kloppy.exceptions import DeserializationError
from kloppy.infra.serializers.event.deserializer import EventDataDeserializer
Expand All @@ -55,6 +57,14 @@
"LA": PositionType.LeftWing,
}

referee_types_mapping: Dict[str, OfficialType] = {
"referee": OfficialType.MainReferee,
"firstAssistant": OfficialType.AssistantReferee,
"videoReferee": OfficialType.VideoAssistantReferee,
"secondAssistant": OfficialType.AssistantReferee,
"fourthOfficial": OfficialType.FourthOfficial,
}

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -102,6 +112,7 @@ class SportecMetadata(NamedTuple):
fps: int
home_coach: str
away_coach: str
officials: List[Official]


def sportec_metadata_from_xml_elm(match_root) -> SportecMetadata:
Expand Down Expand Up @@ -213,6 +224,31 @@ def sportec_metadata_from_xml_elm(match_root) -> SportecMetadata:
]
)

if hasattr(match_root, "MatchInformation") and hasattr(
match_root.MatchInformation, "Referees"
):
officials = []
referee_path = objectify.ObjectPath(
"PutDataRequest.MatchInformation.Referees"
)
referee_elms = referee_path.find(match_root).iterchildren(
tag="Referee"
)

for referee in referee_elms:
ref_attrib = referee.attrib
officials.append(
Official(
official_id=ref_attrib["PersonId"],
name=ref_attrib["Shortname"],
first_name=ref_attrib["FirstName"],
last_name=ref_attrib["LastName"],
role=referee_types_mapping[ref_attrib["Role"]],
)
)
else:
officials = []

return SportecMetadata(
score=score,
teams=teams,
Expand All @@ -222,6 +258,7 @@ def sportec_metadata_from_xml_elm(match_root) -> SportecMetadata:
fps=SPORTEC_FPS,
home_coach=home_coach,
away_coach=away_coach,
officials=officials,
)


Expand Down Expand Up @@ -673,6 +710,7 @@ def deserialize(self, inputs: SportecEventDataInputs) -> EventDataset:
game_id=game_id,
home_coach=home_coach,
away_coach=away_coach,
officials=sportec_metadata.officials,
)

return EventDataset(
Expand Down
10 changes: 10 additions & 0 deletions kloppy/infra/serializers/tracking/sportec/deserializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ def deserialize(
with performance_logging("parse metadata", logger=logger):
sportec_metadata = sportec_metadata_from_xml_elm(match_root)
teams = home_team, away_team = sportec_metadata.teams

periods = sportec_metadata.periods
transformer = self.get_transformer(
pitch_length=sportec_metadata.x_max,
Expand All @@ -130,6 +131,12 @@ def deserialize(
home_coach = sportec_metadata.home_coach
away_coach = sportec_metadata.away_coach

official_ids = []
if sportec_metadata.officials:
official_ids = [
x.official_id for x in sportec_metadata.officials
]

with performance_logging("parse raw data", logger=logger):
date = parse(
match_root.MatchInformation.General.attrib["KickoffTime"]
Expand All @@ -156,6 +163,7 @@ def _iter():
for i, (frame_id, frame_data) in enumerate(
sorted(raw_frames.items())
):

if "ball" not in frame_data:
# Frames without ball data are corrupt.
continue
Expand Down Expand Up @@ -193,6 +201,7 @@ def _iter():
)
for player_id, raw_player_data in frame_data.items()
if player_id != "ball"
and player_id not in official_ids
},
other_data={},
ball_coordinates=Point3D(
Expand Down Expand Up @@ -242,6 +251,7 @@ def _iter():
game_id=game_id,
home_coach=home_coach,
away_coach=away_coach,
officials=sportec_metadata.officials,
)

return TrackingDataset(
Expand Down
Loading

0 comments on commit 45ab84c

Please sign in to comment.