From deb071b0cb1a6126d6c130b3f888134bd4be2fa5 Mon Sep 17 00:00:00 2001 From: "UnravelSports [JB]" Date: Thu, 5 Dec 2024 09:50:59 +0100 Subject: [PATCH 1/9] exclude sportec referees if they exist, plus introduce RefereeType for future use --- kloppy/domain/models/common.py | 21 ++++++++++- kloppy/domain/models/position.py | 7 ++++ .../serializers/event/sportec/deserializer.py | 37 +++++++++++++++++++ .../tracking/sportec/deserializer.py | 7 ++++ 4 files changed, 71 insertions(+), 1 deletion(-) diff --git a/kloppy/domain/models/common.py b/kloppy/domain/models/common.py index c1830d1b..71d4bbaf 100644 --- a/kloppy/domain/models/common.py +++ b/kloppy/domain/models/common.py @@ -18,7 +18,7 @@ Iterable, ) -from .position import PositionType +from .position import PositionType, RefereeType from ...utils import deprecated @@ -119,6 +119,25 @@ def __str__(self): return self.value +@dataclass(frozen=True) +class Referee: + referee_id: str + name: str = None + first_name: str = None + last_name: str = None + role: Optional[RefereeType] = None + + @property + def full_name(self): + if self.name: + return self.name + if self.first_name or self.last_name: + return f"{self.first_name} {self.last_name}" + if self.role: + return f"{self.role}_{self.referee_id}" + return self.referee_id + + @dataclass(frozen=True) class Player: """ diff --git a/kloppy/domain/models/position.py b/kloppy/domain/models/position.py index 84d08280..9c62fe9c 100644 --- a/kloppy/domain/models/position.py +++ b/kloppy/domain/models/position.py @@ -92,3 +92,10 @@ def __str__(self): @classmethod def unknown(cls) -> "PositionType": return cls.Unknown + + +class RefereeType: + VideoReferee = "Video Referee" + Referee = "Referee" + Assistant = "Assistant" + FourthOfficial = "Fourth Official" diff --git a/kloppy/infra/serializers/event/sportec/deserializer.py b/kloppy/infra/serializers/event/sportec/deserializer.py index 14895206..d9831c59 100644 --- a/kloppy/infra/serializers/event/sportec/deserializer.py +++ b/kloppy/infra/serializers/event/sportec/deserializer.py @@ -29,6 +29,8 @@ CardType, AttackingDirection, PositionType, + Referee, + RefereeType, ) from kloppy.exceptions import DeserializationError from kloppy.infra.serializers.event.deserializer import EventDataDeserializer @@ -55,6 +57,14 @@ "LA": PositionType.LeftWing, } +referee_types_mapping: Dict[str, RefereeType] = { + "referee": RefereeType.Referee, + "firstAssistant": RefereeType.Assistant, + "videoReferee": RefereeType.VideoReferee, + "secondAssistant": RefereeType.Assistant, + "fourthOfficial": RefereeType.FourthOfficial, +} + logger = logging.getLogger(__name__) @@ -102,6 +112,7 @@ class SportecMetadata(NamedTuple): fps: int home_coach: str away_coach: str + referees: List[Referee] def sportec_metadata_from_xml_elm(match_root) -> SportecMetadata: @@ -213,6 +224,31 @@ def sportec_metadata_from_xml_elm(match_root) -> SportecMetadata: ] ) + if hasattr(match_root, "MatchInformation") and hasattr( + match_root.MatchInformation, "Referees" + ): + referees = [] + 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 + referees.append( + Referee( + referee_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: + referees = [] + return SportecMetadata( score=score, teams=teams, @@ -222,6 +258,7 @@ def sportec_metadata_from_xml_elm(match_root) -> SportecMetadata: fps=SPORTEC_FPS, home_coach=home_coach, away_coach=away_coach, + referees=referees, ) diff --git a/kloppy/infra/serializers/tracking/sportec/deserializer.py b/kloppy/infra/serializers/tracking/sportec/deserializer.py index 3f418375..bdd18df2 100644 --- a/kloppy/infra/serializers/tracking/sportec/deserializer.py +++ b/kloppy/infra/serializers/tracking/sportec/deserializer.py @@ -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, @@ -130,6 +131,10 @@ def deserialize( home_coach = sportec_metadata.home_coach away_coach = sportec_metadata.away_coach + referee_ids = [] + if sportec_metadata.referees: + referee_ids = [x.referee_id for x in sportec_metadata.referees] + with performance_logging("parse raw data", logger=logger): date = parse( match_root.MatchInformation.General.attrib["KickoffTime"] @@ -156,6 +161,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 @@ -193,6 +199,7 @@ def _iter(): ) for player_id, raw_player_data in frame_data.items() if player_id != "ball" + and player_id not in referee_ids }, other_data={}, ball_coordinates=Point3D( From 21b51c55854486bc63114b6c303fe06aefbc3aa2 Mon Sep 17 00:00:00 2001 From: "UnravelSports [JB]" Date: Thu, 5 Dec 2024 10:05:01 +0100 Subject: [PATCH 2/9] added referee in metadata test --- kloppy/domain/models/common.py | 1 + .../serializers/tracking/sportec/deserializer.py | 1 + kloppy/tests/test_sportec.py | 13 +++++++++++++ 3 files changed, 15 insertions(+) diff --git a/kloppy/domain/models/common.py b/kloppy/domain/models/common.py index 71d4bbaf..27a603ca 100644 --- a/kloppy/domain/models/common.py +++ b/kloppy/domain/models/common.py @@ -1035,6 +1035,7 @@ class Metadata: game_id: Optional[str] = None home_coach: Optional[str] = None away_coach: Optional[str] = None + referees: Optional[List] = field(default_factory=list) attributes: Optional[Dict] = field(default_factory=dict, compare=False) def __post_init__(self): diff --git a/kloppy/infra/serializers/tracking/sportec/deserializer.py b/kloppy/infra/serializers/tracking/sportec/deserializer.py index bdd18df2..f17dbb3d 100644 --- a/kloppy/infra/serializers/tracking/sportec/deserializer.py +++ b/kloppy/infra/serializers/tracking/sportec/deserializer.py @@ -249,6 +249,7 @@ def _iter(): game_id=game_id, home_coach=home_coach, away_coach=away_coach, + referees=sportec_metadata.referees, ) return TrackingDataset( diff --git a/kloppy/tests/test_sportec.py b/kloppy/tests/test_sportec.py index 1c11bb78..b7728fca 100644 --- a/kloppy/tests/test_sportec.py +++ b/kloppy/tests/test_sportec.py @@ -119,6 +119,10 @@ class TestSportecTrackingData: def raw_data(self, base_dir) -> str: return base_dir / "files/sportec_positional.xml" + @pytest.fixture + def raw_data_referee(self, base_dir) -> str: + return base_dir / "files/sportec_positional_w_referee.xml" + @pytest.fixture def meta_data(self, base_dir) -> str: return base_dir / "files/sportec_meta.xml" @@ -238,3 +242,12 @@ def test_enriched_metadata(self, raw_data: Path, meta_data: Path): if away_coach: assert isinstance(away_coach, str) assert away_coach == "M. Rose" + + def test_referees(self, raw_data_referee: Path, meta_data: Path): + dataset = sportec.load_tracking( + raw_data=raw_data_referee, + meta_data=meta_data, + coordinates="sportec", + only_alive=True, + ) + assert len(dataset.metadata.referees) == 4 From a42739307c7e4d5111612521500c782f72226537 Mon Sep 17 00:00:00 2001 From: "UnravelSports [JB]" Date: Thu, 5 Dec 2024 10:16:46 +0100 Subject: [PATCH 3/9] referee testing file --- .../files/sportec_positional_w_referee.xml | 671 ++++++++++++++++++ 1 file changed, 671 insertions(+) create mode 100644 kloppy/tests/files/sportec_positional_w_referee.xml diff --git a/kloppy/tests/files/sportec_positional_w_referee.xml b/kloppy/tests/files/sportec_positional_w_referee.xml new file mode 100644 index 00000000..d9f12d8f --- /dev/null +++ b/kloppy/tests/files/sportec_positional_w_referee.xml @@ -0,0 +1,671 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 4c9410f3950ce3a60fb52a68bfde016ff17cb021 Mon Sep 17 00:00:00 2001 From: "UnravelSports [JB]" Date: Thu, 5 Dec 2024 10:24:38 +0100 Subject: [PATCH 4/9] referee in events parser too --- kloppy/infra/serializers/event/sportec/deserializer.py | 1 + kloppy/tests/test_sportec.py | 1 + 2 files changed, 2 insertions(+) diff --git a/kloppy/infra/serializers/event/sportec/deserializer.py b/kloppy/infra/serializers/event/sportec/deserializer.py index d9831c59..bf184ae9 100644 --- a/kloppy/infra/serializers/event/sportec/deserializer.py +++ b/kloppy/infra/serializers/event/sportec/deserializer.py @@ -710,6 +710,7 @@ def deserialize(self, inputs: SportecEventDataInputs) -> EventDataset: game_id=game_id, home_coach=home_coach, away_coach=away_coach, + referees=sportec_metadata.referees, ) return EventDataset( diff --git a/kloppy/tests/test_sportec.py b/kloppy/tests/test_sportec.py index b7728fca..827ff4dd 100644 --- a/kloppy/tests/test_sportec.py +++ b/kloppy/tests/test_sportec.py @@ -149,6 +149,7 @@ def test_load_metadata(self, raw_data: Path, meta_data: Path): assert dataset.metadata.periods[1].end_timestamp == timedelta( seconds=4000 + 2996.68 ) + assert len(dataset.metadata.referees) == 4 def test_load_frames(self, raw_data: Path, meta_data: Path): dataset = sportec.load_tracking( From 75e1efb8e44393c305956db915ed236234f99ddc Mon Sep 17 00:00:00 2001 From: UnravelSports <64530306+UnravelSports@users.noreply.github.com> Date: Mon, 16 Dec 2024 11:16:36 +0100 Subject: [PATCH 5/9] Update kloppy/domain/models/common.py Co-authored-by: Pieter Robberechts --- kloppy/domain/models/common.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/kloppy/domain/models/common.py b/kloppy/domain/models/common.py index 27a603ca..653ec153 100644 --- a/kloppy/domain/models/common.py +++ b/kloppy/domain/models/common.py @@ -122,9 +122,9 @@ def __str__(self): @dataclass(frozen=True) class Referee: referee_id: str - name: str = None - first_name: str = None - last_name: str = None + name: Optional[str] = None + first_name: Optional[str] = None + last_name: Optional[str] = None role: Optional[RefereeType] = None @property From 3e45ef9482b6f1f4eb9fac70b407146da4eaca9b Mon Sep 17 00:00:00 2001 From: UnravelSports <64530306+UnravelSports@users.noreply.github.com> Date: Mon, 16 Dec 2024 11:16:47 +0100 Subject: [PATCH 6/9] Update kloppy/domain/models/common.py Co-authored-by: Pieter Robberechts --- kloppy/domain/models/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kloppy/domain/models/common.py b/kloppy/domain/models/common.py index 653ec153..003ba028 100644 --- a/kloppy/domain/models/common.py +++ b/kloppy/domain/models/common.py @@ -135,7 +135,7 @@ def full_name(self): return f"{self.first_name} {self.last_name}" if self.role: return f"{self.role}_{self.referee_id}" - return self.referee_id + return f"referee_{self.referee_id}" @dataclass(frozen=True) From 44d2e7a380664315c9875a6019315493126865f6 Mon Sep 17 00:00:00 2001 From: "UnravelSports [JB]" Date: Mon, 16 Dec 2024 11:32:21 +0100 Subject: [PATCH 7/9] converted to Official --- kloppy/domain/models/common.py | 21 ++++++++---- kloppy/domain/models/position.py | 7 ---- .../serializers/event/sportec/deserializer.py | 32 +++++++++---------- .../tracking/sportec/deserializer.py | 12 ++++--- kloppy/tests/test_sportec.py | 4 +-- 5 files changed, 39 insertions(+), 37 deletions(-) diff --git a/kloppy/domain/models/common.py b/kloppy/domain/models/common.py index 003ba028..d042ea07 100644 --- a/kloppy/domain/models/common.py +++ b/kloppy/domain/models/common.py @@ -18,7 +18,7 @@ Iterable, ) -from .position import PositionType, RefereeType +from .position import PositionType from ...utils import deprecated @@ -119,13 +119,20 @@ def __str__(self): return self.value +class OfficialType: + VideoAssistantReferee = "Video Assistant Referee" + MainReferee = "Main Referee" + AssistantReferee = "Assistant Referee" + FourthOfficial = "Fourth Official" + + @dataclass(frozen=True) -class Referee: - referee_id: str +class Official: + official_id: str name: Optional[str] = None first_name: Optional[str] = None last_name: Optional[str] = None - role: Optional[RefereeType] = None + role: Optional[OfficialType] = None @property def full_name(self): @@ -134,8 +141,8 @@ def full_name(self): if self.first_name or self.last_name: return f"{self.first_name} {self.last_name}" if self.role: - return f"{self.role}_{self.referee_id}" - return f"referee_{self.referee_id}" + return f"{self.role}_{self.official_id}" + return f"referee_{self.official_id}" @dataclass(frozen=True) @@ -1035,7 +1042,7 @@ class Metadata: game_id: Optional[str] = None home_coach: Optional[str] = None away_coach: Optional[str] = None - referees: Optional[List] = field(default_factory=list) + officials: Optional[List] = field(default_factory=list) attributes: Optional[Dict] = field(default_factory=dict, compare=False) def __post_init__(self): diff --git a/kloppy/domain/models/position.py b/kloppy/domain/models/position.py index 9c62fe9c..84d08280 100644 --- a/kloppy/domain/models/position.py +++ b/kloppy/domain/models/position.py @@ -92,10 +92,3 @@ def __str__(self): @classmethod def unknown(cls) -> "PositionType": return cls.Unknown - - -class RefereeType: - VideoReferee = "Video Referee" - Referee = "Referee" - Assistant = "Assistant" - FourthOfficial = "Fourth Official" diff --git a/kloppy/infra/serializers/event/sportec/deserializer.py b/kloppy/infra/serializers/event/sportec/deserializer.py index bf184ae9..57d105a4 100644 --- a/kloppy/infra/serializers/event/sportec/deserializer.py +++ b/kloppy/infra/serializers/event/sportec/deserializer.py @@ -29,8 +29,8 @@ CardType, AttackingDirection, PositionType, - Referee, - RefereeType, + Official, + OfficialType, ) from kloppy.exceptions import DeserializationError from kloppy.infra.serializers.event.deserializer import EventDataDeserializer @@ -57,12 +57,12 @@ "LA": PositionType.LeftWing, } -referee_types_mapping: Dict[str, RefereeType] = { - "referee": RefereeType.Referee, - "firstAssistant": RefereeType.Assistant, - "videoReferee": RefereeType.VideoReferee, - "secondAssistant": RefereeType.Assistant, - "fourthOfficial": RefereeType.FourthOfficial, +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__) @@ -112,7 +112,7 @@ class SportecMetadata(NamedTuple): fps: int home_coach: str away_coach: str - referees: List[Referee] + officials: List[Official] def sportec_metadata_from_xml_elm(match_root) -> SportecMetadata: @@ -227,7 +227,7 @@ def sportec_metadata_from_xml_elm(match_root) -> SportecMetadata: if hasattr(match_root, "MatchInformation") and hasattr( match_root.MatchInformation, "Referees" ): - referees = [] + officials = [] referee_path = objectify.ObjectPath( "PutDataRequest.MatchInformation.Referees" ) @@ -237,9 +237,9 @@ def sportec_metadata_from_xml_elm(match_root) -> SportecMetadata: for referee in referee_elms: ref_attrib = referee.attrib - referees.append( - Referee( - referee_id=ref_attrib["PersonId"], + officials.append( + Official( + official_id=ref_attrib["PersonId"], name=ref_attrib["Shortname"], first_name=ref_attrib["FirstName"], last_name=ref_attrib["LastName"], @@ -247,7 +247,7 @@ def sportec_metadata_from_xml_elm(match_root) -> SportecMetadata: ) ) else: - referees = [] + officials = [] return SportecMetadata( score=score, @@ -258,7 +258,7 @@ def sportec_metadata_from_xml_elm(match_root) -> SportecMetadata: fps=SPORTEC_FPS, home_coach=home_coach, away_coach=away_coach, - referees=referees, + officials=officials, ) @@ -710,7 +710,7 @@ def deserialize(self, inputs: SportecEventDataInputs) -> EventDataset: game_id=game_id, home_coach=home_coach, away_coach=away_coach, - referees=sportec_metadata.referees, + officials=sportec_metadata.officials, ) return EventDataset( diff --git a/kloppy/infra/serializers/tracking/sportec/deserializer.py b/kloppy/infra/serializers/tracking/sportec/deserializer.py index f17dbb3d..7cc08516 100644 --- a/kloppy/infra/serializers/tracking/sportec/deserializer.py +++ b/kloppy/infra/serializers/tracking/sportec/deserializer.py @@ -131,9 +131,11 @@ def deserialize( home_coach = sportec_metadata.home_coach away_coach = sportec_metadata.away_coach - referee_ids = [] - if sportec_metadata.referees: - referee_ids = [x.referee_id for x in sportec_metadata.referees] + 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( @@ -199,7 +201,7 @@ def _iter(): ) for player_id, raw_player_data in frame_data.items() if player_id != "ball" - and player_id not in referee_ids + and player_id not in official_ids }, other_data={}, ball_coordinates=Point3D( @@ -249,7 +251,7 @@ def _iter(): game_id=game_id, home_coach=home_coach, away_coach=away_coach, - referees=sportec_metadata.referees, + officials=sportec_metadata.officials, ) return TrackingDataset( diff --git a/kloppy/tests/test_sportec.py b/kloppy/tests/test_sportec.py index 827ff4dd..ac459579 100644 --- a/kloppy/tests/test_sportec.py +++ b/kloppy/tests/test_sportec.py @@ -149,7 +149,7 @@ def test_load_metadata(self, raw_data: Path, meta_data: Path): assert dataset.metadata.periods[1].end_timestamp == timedelta( seconds=4000 + 2996.68 ) - assert len(dataset.metadata.referees) == 4 + assert len(dataset.metadata.officials) == 4 def test_load_frames(self, raw_data: Path, meta_data: Path): dataset = sportec.load_tracking( @@ -251,4 +251,4 @@ def test_referees(self, raw_data_referee: Path, meta_data: Path): coordinates="sportec", only_alive=True, ) - assert len(dataset.metadata.referees) == 4 + assert len(dataset.metadata.officials) == 4 From a541996fdf853131d5f38410350c3188c28f3aef Mon Sep 17 00:00:00 2001 From: "UnravelSports [JB]" Date: Tue, 17 Dec 2024 12:38:43 +0100 Subject: [PATCH 8/9] resolved Enum and full_name behaviour --- kloppy/domain/models/common.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/kloppy/domain/models/common.py b/kloppy/domain/models/common.py index d042ea07..7b0d667e 100644 --- a/kloppy/domain/models/common.py +++ b/kloppy/domain/models/common.py @@ -119,11 +119,11 @@ def __str__(self): return self.value -class OfficialType: - VideoAssistantReferee = "Video Assistant Referee" - MainReferee = "Main Referee" - AssistantReferee = "Assistant Referee" - FourthOfficial = "Fourth Official" +class OfficialType(Enum): + VideoAssistantReferee = "video_assistant_referee" + MainReferee = "main_referee" + AssistantReferee = "assistant_referee" + FourthOfficial = "fourth_official" @dataclass(frozen=True) @@ -138,11 +138,13 @@ class Official: def full_name(self): if self.name: return self.name - if self.first_name or self.last_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"{self.role}_{self.official_id}" - return f"referee_{self.official_id}" + return f"official_{self.official_id}" @dataclass(frozen=True) From e12d22bc93e85c96debefc6d53663e82eafae032 Mon Sep 17 00:00:00 2001 From: "UnravelSports [JB]" Date: Tue, 17 Dec 2024 19:59:07 +0100 Subject: [PATCH 9/9] officials --- kloppy/domain/models/common.py | 24 ++++++++++++++----- kloppy/tests/test_sportec.py | 42 ++++++++++++++++++++++++++++++++++ kloppy/utils.py | 5 ++++ 3 files changed, 65 insertions(+), 6 deletions(-) diff --git a/kloppy/domain/models/common.py b/kloppy/domain/models/common.py index 7b0d667e..b4880451 100644 --- a/kloppy/domain/models/common.py +++ b/kloppy/domain/models/common.py @@ -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 @@ -120,14 +120,23 @@ def __str__(self): class OfficialType(Enum): - VideoAssistantReferee = "video_assistant_referee" - MainReferee = "main_referee" - AssistantReferee = "assistant_referee" - FourthOfficial = "fourth_official" + """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 @@ -136,6 +145,9 @@ class Official: @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: @@ -143,7 +155,7 @@ def full_name(self): if self.last_name: return self.last_name if self.role: - return f"{self.role}_{self.official_id}" + return f"{snake_case(str(self.role))}_{self.official_id}" return f"official_{self.official_id}" diff --git a/kloppy/tests/test_sportec.py b/kloppy/tests/test_sportec.py index ac459579..ac8ad2de 100644 --- a/kloppy/tests/test_sportec.py +++ b/kloppy/tests/test_sportec.py @@ -16,6 +16,8 @@ BallState, Point3D, PositionType, + OfficialType, + Official, ) from kloppy import sportec @@ -252,3 +254,43 @@ def test_referees(self, raw_data_referee: Path, meta_data: Path): only_alive=True, ) assert len(dataset.metadata.officials) == 4 + + assert ( + Official( + official_id="42", + name="Pierluigi Collina", + role=OfficialType.MainReferee, + ).role.value + == "Main Referee" + ) + + assert ( + Official( + official_id="42", + name="Pierluigi Collina", + role=OfficialType.MainReferee, + ).full_name + == "Pierluigi Collina" + ) + assert ( + Official( + official_id="42", + first_name="Pierluigi", + last_name="Collina", + role=OfficialType.MainReferee, + ).full_name + == "Pierluigi Collina" + ) + assert ( + Official( + official_id="42", + last_name="Collina", + role=OfficialType.MainReferee, + ).full_name + == "Collina" + ) + assert ( + Official(official_id="42", role=OfficialType.MainReferee).full_name + == "main_referee_42" + ) + assert Official(official_id="42").full_name == "official_42" diff --git a/kloppy/utils.py b/kloppy/utils.py index b0858398..68d36af2 100644 --- a/kloppy/utils.py +++ b/kloppy/utils.py @@ -169,3 +169,8 @@ def __get__(self, instance, owner): stacklevel=2, ) return self.value + + +def snake_case(s: str) -> str: + """Convert a string to snake_case.""" + return re.sub(r"[\s\-]+", "_", s.strip()).lower()