From 3bca4f659623208356e96e65235a84dcdd443162 Mon Sep 17 00:00:00 2001 From: Pieter Robberechts Date: Thu, 11 May 2023 23:23:54 +0200 Subject: [PATCH 1/6] Fix transform orientation Fixes #175 --- kloppy/domain/models/common.py | 169 +++++++++------ .../domain/services/transformers/dataset.py | 57 ++++-- kloppy/tests/test_helpers.py | 192 ++++++++++++++++-- 3 files changed, 312 insertions(+), 106 deletions(-) diff --git a/kloppy/domain/models/common.py b/kloppy/domain/models/common.py index 485bd067..433e0201 100644 --- a/kloppy/domain/models/common.py +++ b/kloppy/domain/models/common.py @@ -250,6 +250,42 @@ def __repr__(self): return self.value +@dataclass +class Period: + """ + Period + + Attributes: + id: `1` for first half, `2` for second half, `3` for first overtime, + `4` for second overtime, and `5` for penalty shootouts + start_timestamp: timestamp given by provider (can be unix timestamp or relative) + end_timestamp: timestamp given by provider (can be unix timestamp or relative) + attacking_direction: See [`AttackingDirection`][kloppy.domain.models.common.AttackingDirection] + """ + + id: int + start_timestamp: float + end_timestamp: float + attacking_direction: AttackingDirection = AttackingDirection.NOT_SET + + def contains(self, timestamp: float): + return self.start_timestamp <= timestamp <= self.end_timestamp + + @property + def attacking_direction_set(self): + return self.attacking_direction != AttackingDirection.NOT_SET + + def set_attacking_direction(self, attacking_direction: AttackingDirection): + self.attacking_direction = attacking_direction + + @property + def duration(self): + return self.end_timestamp - self.start_timestamp + + def __eq__(self, other): + return isinstance(other, Period) and other.id == self.id + + class Orientation(Enum): # change when possession changes BALL_OWNING_TEAM = "ball-owning-team" @@ -268,50 +304,74 @@ class Orientation(Enum): # Not set in dataset NOT_SET = "not-set" + def get_attacking_direction(self, period: Period) -> AttackingDirection: + if self == Orientation.FIXED_HOME_AWAY: + return AttackingDirection.HOME_AWAY + if self == Orientation.FIXED_AWAY_HOME: + return AttackingDirection.AWAY_HOME + if self == Orientation.HOME_TEAM: + dirmap = { + 1: AttackingDirection.HOME_AWAY, + 2: AttackingDirection.AWAY_HOME, + 3: AttackingDirection.HOME_AWAY, + 4: AttackingDirection.AWAY_HOME, + } + return dirmap.get(period.id, period.attacking_direction) + if self == Orientation.AWAY_TEAM: + dirmap = { + 1: AttackingDirection.AWAY_HOME, + 2: AttackingDirection.HOME_AWAY, + 3: AttackingDirection.AWAY_HOME, + 4: AttackingDirection.HOME_AWAY, + } + return dirmap.get(period.id, period.attacking_direction) + return AttackingDirection.NOT_SET + def get_orientation_factor( self, - attacking_direction: AttackingDirection, + period: Period, ball_owning_team: Team, action_executing_team: Team, ) -> int: + if period.id == 5: + return 1 # the orientation of penalty shootouts is not transformed if self == Orientation.FIXED_HOME_AWAY: - return -1 - elif self == Orientation.FIXED_AWAY_HOME: return 1 - elif self == Orientation.HOME_TEAM: - if attacking_direction == AttackingDirection.HOME_AWAY: - return -1 - elif attacking_direction == AttackingDirection.AWAY_HOME: + if self == Orientation.FIXED_AWAY_HOME: + return -1 + if self == Orientation.HOME_TEAM: + if period.id == 1 or period.id == 3: return 1 - else: - raise OrientationError("AttackingDirection not set") - elif self == Orientation.AWAY_TEAM: - if attacking_direction == AttackingDirection.AWAY_HOME: + if period.id == 2 or period.id == 4: return -1 - elif attacking_direction == AttackingDirection.HOME_AWAY: + raise OrientationError( + f"AttackingDirection not defined for period with id {period.id}" + ) + if self == Orientation.AWAY_TEAM: + if period.id == 1 or period.id == 3: + return -1 + if period.id == 2 or period.id == 4: return 1 - else: - raise OrientationError("AttackingDirection not set") - elif self == Orientation.BALL_OWNING_TEAM: + raise OrientationError( + f"AttackingDirection not defined for period with id {period.id}" + ) + if self == Orientation.BALL_OWNING_TEAM: if ball_owning_team.ground == Ground.HOME: - return -1 - elif ball_owning_team.ground == Ground.AWAY: return 1 - else: - raise OrientationError( - f"Invalid ball_owning_team: {ball_owning_team}" - ) - elif self == Orientation.ACTION_EXECUTING_TEAM: - if action_executing_team.ground == Ground.HOME: + if ball_owning_team.ground == Ground.AWAY: return -1 - elif action_executing_team.ground == Ground.AWAY: + raise OrientationError( + f"Invalid ball_owning_team: {ball_owning_team}" + ) + if self == Orientation.ACTION_EXECUTING_TEAM: + if action_executing_team.ground == Ground.HOME: return 1 - else: - raise OrientationError( - f"Invalid action_executing_team: {action_executing_team}" - ) - else: - raise OrientationError(f"Unknown orientation: {self}") + if action_executing_team.ground == Ground.AWAY: + return -1 + raise OrientationError( + f"Invalid action_executing_team: {action_executing_team}" + ) + raise OrientationError(f"Unknown orientation: {self}") def __repr__(self): return self.value @@ -325,43 +385,6 @@ class VerticalOrientation(Enum): BOTTOM_TO_TOP = "bottom-to-top" -@dataclass -class Period: - """ - Period - - Attributes: - id: `1` for first half, `2` for second half - start_timestamp: timestamp given by provider (can be unix timestamp or relative) - end_timestamp: timestamp given by provider (can be unix timestamp or relative) - attacking_direction: See [`AttackingDirection`][kloppy.domain.models.common.AttackingDirection] - """ - - id: int - start_timestamp: float - end_timestamp: float - attacking_direction: Optional[ - AttackingDirection - ] = AttackingDirection.NOT_SET - - def contains(self, timestamp: float): - return self.start_timestamp <= timestamp <= self.end_timestamp - - @property - def attacking_direction_set(self): - return self.attacking_direction != AttackingDirection.NOT_SET - - def set_attacking_direction(self, attacking_direction: AttackingDirection): - self.attacking_direction = attacking_direction - - @property - def duration(self): - return self.end_timestamp - self.start_timestamp - - def __eq__(self, other): - return isinstance(other, Period) and other.id == self.id - - class Origin(Enum): """ Attributes: @@ -848,6 +871,18 @@ class Metadata: frame_rate: Optional[float] = None attributes: Optional[Dict] = field(default_factory=dict, compare=False) + def __post_init__(self): + if self.coordinate_system is not None: + # set the pitch dimensions from the coordinate system + self.pitch_dimensions = self.coordinate_system.pitch_dimensions + + if self.orientation is not None: + # set the attacking directions from the orientation + for period in self.periods: + period.attacking_direction = ( + self.orientation.get_attacking_direction(period) + ) + T = TypeVar("T", bound="DataRecord") diff --git a/kloppy/domain/services/transformers/dataset.py b/kloppy/domain/services/transformers/dataset.py index 3ddb1d46..e9507937 100644 --- a/kloppy/domain/services/transformers/dataset.py +++ b/kloppy/domain/services/transformers/dataset.py @@ -7,10 +7,12 @@ AttackingDirection, Dataset, DatasetFlag, + DataRecord, EventDataset, Frame, Orientation, PitchDimensions, + Period, Point, Point3D, Team, @@ -90,6 +92,10 @@ def _needs_coordinate_system_change(self): def _needs_pitch_dimensions_change(self): return self._from_pitch_dimensions != self._to_pitch_dimensions + @property + def _needs_orientation_change(self): + return self._from_orientation != self._to_orientation + def change_point_dimensions( self, point: Union[Point, Point3D, None] ) -> Union[Point, Point3D, None]: @@ -130,7 +136,7 @@ def flip_point( def __needs_flip( self, ball_owning_team: Team, - attacking_direction: AttackingDirection, + period: Period, action_executing_team: Optional[Team] = None, ) -> bool: if ( @@ -146,14 +152,14 @@ def __needs_flip( orientation_factor_from = ( self._from_orientation.get_orientation_factor( ball_owning_team=ball_owning_team, - attacking_direction=attacking_direction, + period=period, action_executing_team=action_executing_team, ) ) orientation_factor_to = ( self._to_orientation.get_orientation_factor( ball_owning_team=ball_owning_team, - attacking_direction=attacking_direction, + period=period, action_executing_team=action_executing_team, ) ) @@ -164,16 +170,20 @@ def transform_frame(self, frame: Frame) -> Frame: # Change coordinate system if self._needs_coordinate_system_change: frame = self.__change_frame_coordinate_system(frame) + # Change dimensions elif self._needs_pitch_dimensions_change: frame = self.__change_frame_dimensions(frame) # Flip frame based on orientation - if self.__needs_flip( - ball_owning_team=frame.ball_owning_team, - attacking_direction=frame.period.attacking_direction, - ): - frame = self.__flip_frame(frame) + if self._needs_orientation_change: + if self.__needs_flip( + ball_owning_team=frame.ball_owning_team, + period=frame.period, + ): + frame = self.__flip_frame(frame) + + frame = self.__change_attacking_direction(frame) return frame @@ -254,6 +264,15 @@ def __change_point_coordinate_system( else: return Point(x=x, y=y) + def __change_attacking_direction(self, record: DataRecord): + new_attacking_direction = self._to_orientation.get_attacking_direction( + record.period + ) + period = replace( + record.period, attacking_direction=new_attacking_direction + ) + return replace(record, period=period) + def __flip_frame(self, frame: Frame): players_data = {} for player, data in frame.players_data.items(): @@ -281,20 +300,24 @@ def transform_event(self, event: Event) -> Event: # Change coordinate system if self._needs_coordinate_system_change: event = self.__change_event_coordinate_system(event) + # Change dimensions elif self._needs_pitch_dimensions_change: event = self.__change_event_dimensions(event) # Flip event based on orientation - if self.__needs_flip( - ball_owning_team=event.ball_owning_team, - attacking_direction=event.period.attacking_direction, - action_executing_team=event.team, - ): - event = self.__flip_event(event) - - if event.freeze_frame: - event.freeze_frame = self.transform_frame(event.freeze_frame) + if self._needs_orientation_change: + if self.__needs_flip( + ball_owning_team=event.ball_owning_team, + period=event.period, + action_executing_team=event.team, + ): + event = self.__flip_event(event) + + if event.freeze_frame: + event.freeze_frame = self.transform_frame(event.freeze_frame) + + event = self.__change_attacking_direction(event) return event diff --git a/kloppy/tests/test_helpers.py b/kloppy/tests/test_helpers.py index ddf4c8a2..f95a0753 100644 --- a/kloppy/tests/test_helpers.py +++ b/kloppy/tests/test_helpers.py @@ -54,7 +54,7 @@ def _get_tracking_dataset(self): ), ] metadata = Metadata( - flags=~(DatasetFlag.BALL_OWNING_TEAM | DatasetFlag.BALL_STATE), + flags=(DatasetFlag.BALL_OWNING_TEAM), pitch_dimensions=PitchDimensions( x_dim=Dimension(0, 100), y_dim=Dimension(-50, 50) ), @@ -73,7 +73,7 @@ def _get_tracking_dataset(self): Frame( frame_id=1, timestamp=0.1, - ball_owning_team=None, + ball_owning_team=teams[0], ball_state=None, period=periods[0], players_data={}, @@ -83,9 +83,9 @@ def _get_tracking_dataset(self): Frame( frame_id=2, timestamp=0.2, - ball_owning_team=None, + ball_owning_team=teams[1], ball_state=None, - period=periods[0], + period=periods[1], players_data={ Player( team=home_team, player_id="home_1", jersey_no=1 @@ -129,19 +129,6 @@ def test_transform(self): ) ) - def test_transform_to_orientation(self): - tracking_data = self._get_tracking_dataset() - - transformed_dataset = tracking_data.transform( - to_orientation=Orientation.AWAY_TEAM, - ) - assert transformed_dataset.frames[0].ball_coordinates == Point3D( - x=0, y=50, z=0 - ) - assert ( - transformed_dataset.metadata.orientation == Orientation.AWAY_TEAM - ) - def test_transform_to_pitch_dimensions(self): tracking_data = self._get_tracking_dataset() @@ -164,7 +151,168 @@ def test_transform_to_pitch_dimensions(self): ) ) - def test_transform_to_coordinate_system(self, base_dir): + def test_transform_to_orientation(self): + tracking_data = self._get_tracking_dataset() + + original = tracking_data.transform( + to_pitch_dimensions=[[0, 1], [0, 1]], + ) + assert original.frames[0].ball_coordinates == Point3D(x=1, y=0, z=0) + assert ( + original.frames[0].period.attacking_direction + == AttackingDirection.HOME_AWAY + ) + assert original.frames[1].ball_coordinates == Point3D(x=0, y=1, z=1) + assert ( + original.frames[1].period.attacking_direction + == AttackingDirection.AWAY_HOME + ) + assert original.metadata.orientation == Orientation.HOME_TEAM + assert ( + original.metadata.periods[0].attacking_direction + == AttackingDirection.HOME_AWAY + ) + assert ( + original.metadata.periods[1].attacking_direction + == AttackingDirection.AWAY_HOME + ) + + print("T1") + transform1 = original.transform( + to_orientation=Orientation.AWAY_TEAM, + to_pitch_dimensions=[[0, 1], [0, 1]], + ) + # all coordinates should be flipped + assert transform1.frames[0].ball_coordinates == Point3D(x=0, y=1, z=0) + assert ( + transform1.frames[0].period.attacking_direction + == AttackingDirection.AWAY_HOME + ) + assert transform1.frames[1].ball_coordinates == Point3D(x=1, y=0, z=1) + assert ( + transform1.frames[1].period.attacking_direction + == AttackingDirection.HOME_AWAY + ) + assert transform1.metadata.orientation == Orientation.AWAY_TEAM + assert ( + transform1.metadata.periods[0].attacking_direction + == AttackingDirection.AWAY_HOME + ) + assert ( + transform1.metadata.periods[1].attacking_direction + == AttackingDirection.HOME_AWAY + ) + + print("T2") + transform2 = transform1.transform( + to_orientation=Orientation.FIXED_AWAY_HOME, + to_pitch_dimensions=[[0, 1], [0, 1]], + ) + # all coordintes in the second half should be flipped + assert transform2.frames[0].ball_coordinates == Point3D(x=0, y=1, z=0) + assert ( + transform2.frames[0].period.attacking_direction + == AttackingDirection.AWAY_HOME + ) + assert transform2.frames[1].ball_coordinates == Point3D(x=0, y=1, z=1) + assert ( + transform2.frames[1].period.attacking_direction + == AttackingDirection.AWAY_HOME + ) + assert transform2.metadata.orientation == Orientation.FIXED_AWAY_HOME + assert ( + transform2.metadata.periods[0].attacking_direction + == AttackingDirection.AWAY_HOME + ) + assert ( + transform2.metadata.periods[1].attacking_direction + == AttackingDirection.AWAY_HOME + ) + + print("T3") + transform3 = transform2.transform( + to_orientation=Orientation.BALL_OWNING_TEAM, + to_pitch_dimensions=[[0, 1], [0, 1]], + ) + # the coordinates of frame 1 should be flipped + assert transform3.frames[0].ball_coordinates == Point3D(x=1, y=0, z=0) + assert ( + transform3.frames[0].period.attacking_direction + == AttackingDirection.NOT_SET + ) + assert transform3.frames[1].ball_coordinates == Point3D(x=0, y=1, z=1) + assert ( + transform3.frames[1].period.attacking_direction + == AttackingDirection.NOT_SET + ) + assert transform3.metadata.orientation == Orientation.BALL_OWNING_TEAM + assert ( + transform3.metadata.periods[0].attacking_direction + == AttackingDirection.NOT_SET + ) + assert ( + transform3.metadata.periods[1].attacking_direction + == AttackingDirection.NOT_SET + ) + + print("T4") + transform4 = transform2.transform( + to_orientation=Orientation.ACTION_EXECUTING_TEAM, + to_pitch_dimensions=[[0, 1], [0, 1]], + ) + # should be identical to transform3 as the action_executing team is not defined + assert transform4.frames[0].ball_coordinates == Point3D(x=1, y=0, z=0) + assert ( + transform4.frames[0].period.attacking_direction + == AttackingDirection.NOT_SET + ) + assert transform4.frames[1].ball_coordinates == Point3D(x=0, y=1, z=1) + assert ( + transform4.frames[1].period.attacking_direction + == AttackingDirection.NOT_SET + ) + assert ( + transform4.metadata.orientation + == Orientation.ACTION_EXECUTING_TEAM + ) + assert ( + transform4.metadata.periods[0].attacking_direction + == AttackingDirection.NOT_SET + ) + assert ( + transform4.metadata.periods[1].attacking_direction + == AttackingDirection.NOT_SET + ) + + print("T5") + transform5 = transform4.transform( + to_orientation=Orientation.HOME_TEAM, + to_pitch_dimensions=[[0, 1], [0, 1]], + ) + # we should be back at the original + assert transform5.frames[0].ball_coordinates == Point3D(x=1, y=0, z=0) + assert ( + transform5.frames[0].period.attacking_direction + == AttackingDirection.HOME_AWAY + ) + assert transform5.frames[1].ball_coordinates == Point3D(x=0, y=1, z=1) + assert ( + transform5.frames[1].period.attacking_direction + == AttackingDirection.AWAY_HOME + ) + assert transform5.metadata.orientation == Orientation.HOME_TEAM + assert ( + transform5.metadata.periods[0].attacking_direction + == AttackingDirection.HOME_AWAY + ) + assert ( + transform5.metadata.periods[1].attacking_direction + == AttackingDirection.AWAY_HOME + ) + + def test_transform_to_coordinate_system(self): + base_dir = os.path.dirname(__file__) + dataset = tracab.load( meta_data=base_dir / "files/tracab_meta.xml", raw_data=base_dir / "files/tracab_raw.dat", @@ -289,10 +437,10 @@ def test_to_pandas(self): expected_data_frame = DataFrame.from_dict( { "frame_id": {0: 1, 1: 2}, - "period_id": {0: 1, 1: 1}, + "period_id": {0: 1, 1: 2}, "timestamp": {0: 0.1, 1: 0.2}, "ball_state": {0: None, 1: None}, - "ball_owning_team_id": {0: None, 1: None}, + "ball_owning_team_id": {0: "home", 1: "away"}, "ball_x": {0: 100, 1: 0}, "ball_y": {0: -50, 1: 50}, "ball_z": {0: 0, 1: 1}, @@ -342,10 +490,10 @@ def test_to_pandas_additional_columns(self): expected_data_frame = DataFrame.from_dict( { "frame_id": [1, 2], - "period_id": [1, 1], + "period_id": [1, 2], "timestamp": [0.1, 0.2], "ball_state": [None, None], - "ball_owning_team_id": [None, None], + "ball_owning_team_id": ["home", "away"], "ball_x": [100, 0], "ball_y": [-50, 50], "ball_z": [0, 1], From b02768db9a4029aff7d4644ad8ff5ba1c47b0ab4 Mon Sep 17 00:00:00 2001 From: Pieter Robberechts Date: Tue, 16 May 2023 16:04:34 +0200 Subject: [PATCH 2/6] Refactor orientation transform unit test --- kloppy/tests/test_helpers.py | 142 ++++++++++++++--------------------- 1 file changed, 57 insertions(+), 85 deletions(-) diff --git a/kloppy/tests/test_helpers.py b/kloppy/tests/test_helpers.py index f95a0753..d093c824 100644 --- a/kloppy/tests/test_helpers.py +++ b/kloppy/tests/test_helpers.py @@ -152,22 +152,24 @@ def test_transform_to_pitch_dimensions(self): ) def test_transform_to_orientation(self): - tracking_data = self._get_tracking_dataset() - - original = tracking_data.transform( + # Create a dataset with the KLOPPY pitch dimensions + # and HOME_TEAM orientation + original = self._get_tracking_dataset().transform( to_pitch_dimensions=[[0, 1], [0, 1]], ) + assert original.metadata.orientation == Orientation.HOME_TEAM assert original.frames[0].ball_coordinates == Point3D(x=1, y=0, z=0) + assert original.frames[1].ball_coordinates == Point3D(x=0, y=1, z=1) + # the frames should have the correct attacking direction assert ( original.frames[0].period.attacking_direction == AttackingDirection.HOME_AWAY ) - assert original.frames[1].ball_coordinates == Point3D(x=0, y=1, z=1) assert ( original.frames[1].period.attacking_direction == AttackingDirection.AWAY_HOME ) - assert original.metadata.orientation == Orientation.HOME_TEAM + # the metadata should have the correct attacking direction assert ( original.metadata.periods[0].attacking_direction == AttackingDirection.HOME_AWAY @@ -177,23 +179,25 @@ def test_transform_to_orientation(self): == AttackingDirection.AWAY_HOME ) - print("T1") + # Transform to AWAY_TEAM orientation transform1 = original.transform( to_orientation=Orientation.AWAY_TEAM, to_pitch_dimensions=[[0, 1], [0, 1]], ) + assert transform1.metadata.orientation == Orientation.AWAY_TEAM # all coordinates should be flipped assert transform1.frames[0].ball_coordinates == Point3D(x=0, y=1, z=0) + assert transform1.frames[1].ball_coordinates == Point3D(x=1, y=0, z=1) + # the frames should have the correct attacking direction assert ( transform1.frames[0].period.attacking_direction == AttackingDirection.AWAY_HOME ) - assert transform1.frames[1].ball_coordinates == Point3D(x=1, y=0, z=1) assert ( transform1.frames[1].period.attacking_direction == AttackingDirection.HOME_AWAY ) - assert transform1.metadata.orientation == Orientation.AWAY_TEAM + # the metadata should have the correct attacking direction assert ( transform1.metadata.periods[0].attacking_direction == AttackingDirection.AWAY_HOME @@ -203,112 +207,80 @@ def test_transform_to_orientation(self): == AttackingDirection.HOME_AWAY ) - print("T2") + # Transform to FIXED_AWAY_HOME orientation transform2 = transform1.transform( to_orientation=Orientation.FIXED_AWAY_HOME, to_pitch_dimensions=[[0, 1], [0, 1]], ) + assert transform2.metadata.orientation == Orientation.FIXED_AWAY_HOME # all coordintes in the second half should be flipped assert transform2.frames[0].ball_coordinates == Point3D(x=0, y=1, z=0) - assert ( - transform2.frames[0].period.attacking_direction - == AttackingDirection.AWAY_HOME - ) assert transform2.frames[1].ball_coordinates == Point3D(x=0, y=1, z=1) - assert ( - transform2.frames[1].period.attacking_direction - == AttackingDirection.AWAY_HOME - ) - assert transform2.metadata.orientation == Orientation.FIXED_AWAY_HOME - assert ( - transform2.metadata.periods[0].attacking_direction - == AttackingDirection.AWAY_HOME - ) - assert ( - transform2.metadata.periods[1].attacking_direction - == AttackingDirection.AWAY_HOME - ) + # the frames should have the correct attacking direction + for frame in transform2.frames: + assert ( + frame.period.attacking_direction + == AttackingDirection.AWAY_HOME + ) + # the metadata should have the correct attacking direction + for period in transform2.metadata.periods: + assert period.attacking_direction == AttackingDirection.AWAY_HOME - print("T3") + # Transform to BALL_OWNING_TEAM orientation transform3 = transform2.transform( to_orientation=Orientation.BALL_OWNING_TEAM, to_pitch_dimensions=[[0, 1], [0, 1]], ) + assert transform3.metadata.orientation == Orientation.BALL_OWNING_TEAM # the coordinates of frame 1 should be flipped assert transform3.frames[0].ball_coordinates == Point3D(x=1, y=0, z=0) - assert ( - transform3.frames[0].period.attacking_direction - == AttackingDirection.NOT_SET - ) assert transform3.frames[1].ball_coordinates == Point3D(x=0, y=1, z=1) - assert ( - transform3.frames[1].period.attacking_direction - == AttackingDirection.NOT_SET - ) - assert transform3.metadata.orientation == Orientation.BALL_OWNING_TEAM - assert ( - transform3.metadata.periods[0].attacking_direction - == AttackingDirection.NOT_SET - ) - assert ( - transform3.metadata.periods[1].attacking_direction - == AttackingDirection.NOT_SET - ) + # the frames should have the correct attacking direction + for frame in transform3.frames: + assert ( + frame.period.attacking_direction == AttackingDirection.NOT_SET + ) + # the metadata should have the correct attacking direction + for period in transform3.metadata.periods: + assert period.attacking_direction == AttackingDirection.NOT_SET - print("T4") - transform4 = transform2.transform( + # Transform to ACTION_EXECUTING_TEAM orientation + transform4 = transform3.transform( to_orientation=Orientation.ACTION_EXECUTING_TEAM, to_pitch_dimensions=[[0, 1], [0, 1]], ) - # should be identical to transform3 as the action_executing team is not defined - assert transform4.frames[0].ball_coordinates == Point3D(x=1, y=0, z=0) - assert ( - transform4.frames[0].period.attacking_direction - == AttackingDirection.NOT_SET - ) - assert transform4.frames[1].ball_coordinates == Point3D(x=0, y=1, z=1) - assert ( - transform4.frames[1].period.attacking_direction - == AttackingDirection.NOT_SET - ) assert ( transform4.metadata.orientation == Orientation.ACTION_EXECUTING_TEAM ) - assert ( - transform4.metadata.periods[0].attacking_direction - == AttackingDirection.NOT_SET - ) - assert ( - transform4.metadata.periods[1].attacking_direction - == AttackingDirection.NOT_SET - ) + # should be identical to transform3 as the action_executing team is not defined + assert transform4.frames[0].ball_coordinates == Point3D(x=1, y=0, z=0) + # the frames should have the correct attacking direction + assert transform4.frames[1].ball_coordinates == Point3D(x=0, y=1, z=1) + for frame in transform4.frames: + assert ( + frame.period.attacking_direction == AttackingDirection.NOT_SET + ) + # the metadata should have the correct attacking direction + for period in transform4.metadata.periods: + assert period.attacking_direction == AttackingDirection.NOT_SET - print("T5") + # Transform back to the original HOME_TEAM orientation transform5 = transform4.transform( to_orientation=Orientation.HOME_TEAM, to_pitch_dimensions=[[0, 1], [0, 1]], ) # we should be back at the original - assert transform5.frames[0].ball_coordinates == Point3D(x=1, y=0, z=0) - assert ( - transform5.frames[0].period.attacking_direction - == AttackingDirection.HOME_AWAY - ) - assert transform5.frames[1].ball_coordinates == Point3D(x=0, y=1, z=1) - assert ( - transform5.frames[1].period.attacking_direction - == AttackingDirection.AWAY_HOME - ) - assert transform5.metadata.orientation == Orientation.HOME_TEAM - assert ( - transform5.metadata.periods[0].attacking_direction - == AttackingDirection.HOME_AWAY - ) - assert ( - transform5.metadata.periods[1].attacking_direction - == AttackingDirection.AWAY_HOME - ) + for frame1, frame2 in zip(original.frames, transform5.frames): + assert frame1.ball_coordinates == frame2.ball_coordinates + assert ( + frame1.period.attacking_direction + == frame2.period.attacking_direction + ) + for period1, period2 in zip( + original.metadata.periods, transform5.metadata.periods + ): + assert period1.attacking_direction == period2.attacking_direction def test_transform_to_coordinate_system(self): base_dir = os.path.dirname(__file__) From 0d5e22ca28132b9b0efad085bf385e9a8ef89d29 Mon Sep 17 00:00:00 2001 From: Pieter Robberechts Date: Fri, 1 Sep 2023 19:40:21 +0200 Subject: [PATCH 3/6] Rename HOME_TEAM and AWAY_TEAM orientations --- kloppy/domain/models/common.py | 12 ++++++------ kloppy/tests/test_helpers.py | 20 ++++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/kloppy/domain/models/common.py b/kloppy/domain/models/common.py index 433e0201..bd439779 100644 --- a/kloppy/domain/models/common.py +++ b/kloppy/domain/models/common.py @@ -294,8 +294,8 @@ class Orientation(Enum): ACTION_EXECUTING_TEAM = "action-executing-team" # changes during half-time - HOME_TEAM = "home-team" - AWAY_TEAM = "away-team" + HOME_AWAY = "home-away" + AWAY_HOME = "away-home" # won't change during match FIXED_HOME_AWAY = "fixed-home-away" @@ -309,7 +309,7 @@ def get_attacking_direction(self, period: Period) -> AttackingDirection: return AttackingDirection.HOME_AWAY if self == Orientation.FIXED_AWAY_HOME: return AttackingDirection.AWAY_HOME - if self == Orientation.HOME_TEAM: + if self == Orientation.HOME_AWAY: dirmap = { 1: AttackingDirection.HOME_AWAY, 2: AttackingDirection.AWAY_HOME, @@ -317,7 +317,7 @@ def get_attacking_direction(self, period: Period) -> AttackingDirection: 4: AttackingDirection.AWAY_HOME, } return dirmap.get(period.id, period.attacking_direction) - if self == Orientation.AWAY_TEAM: + if self == Orientation.AWAY_HOME: dirmap = { 1: AttackingDirection.AWAY_HOME, 2: AttackingDirection.HOME_AWAY, @@ -339,7 +339,7 @@ def get_orientation_factor( return 1 if self == Orientation.FIXED_AWAY_HOME: return -1 - if self == Orientation.HOME_TEAM: + if self == Orientation.HOME_AWAY: if period.id == 1 or period.id == 3: return 1 if period.id == 2 or period.id == 4: @@ -347,7 +347,7 @@ def get_orientation_factor( raise OrientationError( f"AttackingDirection not defined for period with id {period.id}" ) - if self == Orientation.AWAY_TEAM: + if self == Orientation.AWAY_HOME: if period.id == 1 or period.id == 3: return -1 if period.id == 2 or period.id == 4: diff --git a/kloppy/tests/test_helpers.py b/kloppy/tests/test_helpers.py index d093c824..b40b9e47 100644 --- a/kloppy/tests/test_helpers.py +++ b/kloppy/tests/test_helpers.py @@ -58,7 +58,7 @@ def _get_tracking_dataset(self): pitch_dimensions=PitchDimensions( x_dim=Dimension(0, 100), y_dim=Dimension(-50, 50) ), - orientation=Orientation.HOME_TEAM, + orientation=Orientation.HOME_AWAY, frame_rate=25, periods=periods, teams=teams, @@ -108,7 +108,7 @@ def test_transform(self): # orientation change AND dimension scale transformed_dataset = tracking_data.transform( - to_orientation="AWAY_TEAM", + to_orientation="AWAY_HOME", to_pitch_dimensions=[[0, 1], [0, 1]], ) @@ -119,7 +119,7 @@ def test_transform(self): x=1, y=0, z=1 ) assert ( - transformed_dataset.metadata.orientation == Orientation.AWAY_TEAM + transformed_dataset.metadata.orientation == Orientation.AWAY_HOME ) assert transformed_dataset.metadata.coordinate_system is None assert ( @@ -153,11 +153,11 @@ def test_transform_to_pitch_dimensions(self): def test_transform_to_orientation(self): # Create a dataset with the KLOPPY pitch dimensions - # and HOME_TEAM orientation + # and HOME_AWAY orientation original = self._get_tracking_dataset().transform( to_pitch_dimensions=[[0, 1], [0, 1]], ) - assert original.metadata.orientation == Orientation.HOME_TEAM + assert original.metadata.orientation == Orientation.HOME_AWAY assert original.frames[0].ball_coordinates == Point3D(x=1, y=0, z=0) assert original.frames[1].ball_coordinates == Point3D(x=0, y=1, z=1) # the frames should have the correct attacking direction @@ -179,12 +179,12 @@ def test_transform_to_orientation(self): == AttackingDirection.AWAY_HOME ) - # Transform to AWAY_TEAM orientation + # Transform to AWAY_HOME orientation transform1 = original.transform( - to_orientation=Orientation.AWAY_TEAM, + to_orientation=Orientation.AWAY_HOME, to_pitch_dimensions=[[0, 1], [0, 1]], ) - assert transform1.metadata.orientation == Orientation.AWAY_TEAM + assert transform1.metadata.orientation == Orientation.AWAY_HOME # all coordinates should be flipped assert transform1.frames[0].ball_coordinates == Point3D(x=0, y=1, z=0) assert transform1.frames[1].ball_coordinates == Point3D(x=1, y=0, z=1) @@ -265,9 +265,9 @@ def test_transform_to_orientation(self): for period in transform4.metadata.periods: assert period.attacking_direction == AttackingDirection.NOT_SET - # Transform back to the original HOME_TEAM orientation + # Transform back to the original HOME_AWAY orientation transform5 = transform4.transform( - to_orientation=Orientation.HOME_TEAM, + to_orientation=Orientation.HOME_AWAY, to_pitch_dimensions=[[0, 1], [0, 1]], ) # we should be back at the original From 459c007181687d6b8d055849217311fbdca3890c Mon Sep 17 00:00:00 2001 From: Pieter Robberechts Date: Thu, 7 Sep 2023 09:38:51 +0200 Subject: [PATCH 4/6] New Orientation and AttackingDirection API --- kloppy/domain/models/common.py | 227 +++++++++++------- kloppy/domain/models/event.py | 24 +- kloppy/domain/services/__init__.py | 6 +- .../domain/services/transformers/dataset.py | 40 ++- .../event/datafactory/deserializer.py | 5 +- .../serializers/event/sportec/deserializer.py | 28 +-- .../event/statsbomb/deserializer.py | 2 +- .../infra/serializers/tracking/metrica_csv.py | 28 ++- .../tracking/metrica_epts/metadata.py | 69 +++--- .../serializers/tracking/secondspectrum.py | 28 ++- .../infra/serializers/tracking/skillcorner.py | 60 ++--- .../tracking/sportec/deserializer.py | 29 ++- .../serializers/tracking/statsperform.py | 28 ++- kloppy/infra/serializers/tracking/tracab.py | 29 ++- kloppy/tests/test_datafactory.py | 4 +- kloppy/tests/test_helpers.py | 84 ++----- kloppy/tests/test_metrica_csv.py | 4 +- kloppy/tests/test_metrica_epts.py | 2 +- kloppy/tests/test_metrica_events.py | 4 +- kloppy/tests/test_opta.py | 3 - kloppy/tests/test_secondspectrum.py | 10 +- kloppy/tests/test_skillcorner.py | 4 +- kloppy/tests/test_sportec.py | 4 +- kloppy/tests/test_statsbomb.py | 8 - kloppy/tests/test_statsperform.py | 10 +- kloppy/tests/test_tracab.py | 4 +- 26 files changed, 351 insertions(+), 393 deletions(-) diff --git a/kloppy/domain/models/common.py b/kloppy/domain/models/common.py index bd439779..f6839c32 100644 --- a/kloppy/domain/models/common.py +++ b/kloppy/domain/models/common.py @@ -232,24 +232,6 @@ def __repr__(self): return self.value -class AttackingDirection(Enum): - """ - AttackingDirection - - Attributes: - HOME_AWAY (AttackingDirection): Home team is playing from left to right - AWAY_HOME (AttackingDirection): Home team is playing from right to left - NOT_SET (AttackingDirection): not set yet - """ - - HOME_AWAY = "home-away" - AWAY_HOME = "away-home" - NOT_SET = "not-set" - - def __repr__(self): - return self.value - - @dataclass class Period: """ @@ -260,24 +242,15 @@ class Period: `4` for second overtime, and `5` for penalty shootouts start_timestamp: timestamp given by provider (can be unix timestamp or relative) end_timestamp: timestamp given by provider (can be unix timestamp or relative) - attacking_direction: See [`AttackingDirection`][kloppy.domain.models.common.AttackingDirection] """ id: int start_timestamp: float end_timestamp: float - attacking_direction: AttackingDirection = AttackingDirection.NOT_SET def contains(self, timestamp: float): return self.start_timestamp <= timestamp <= self.end_timestamp - @property - def attacking_direction_set(self): - return self.attacking_direction != AttackingDirection.NOT_SET - - def set_attacking_direction(self, attacking_direction: AttackingDirection): - self.attacking_direction = attacking_direction - @property def duration(self): return self.end_timestamp - self.start_timestamp @@ -287,6 +260,31 @@ def __eq__(self, other): class Orientation(Enum): + """ + The attacking direction of each team in a dataset. + + Attributes: + BALL_OWNING_TEAM: The team that is currently in possession of the ball + plays from left to right. + ACTION_EXECUTING_TEAM: The team that executes the action + plays from left to right. Used in event stream data only. Equivalent + to "BALL_OWNING_TEAM" for tracking data. + HOME_AWAY: The home team plays from left to right in the first period. + The away team plays from left to right in the second period. + AWAY_HOME: The away team plays from left to right in the first period. + The home team plays from left to right in the second period. + FIXED_HOME_AWAY: The home team plays from left to right in both periods. + FIXED_AWAY_HOME: The away team plays from left to right in both periods. + NOT_SET: The attacking direction is not defined. + + Notes: + The attacking direction is not defined for penalty shootouts in the + `HOME_AWAY`, `AWAY_HOME`, `FIXED_HOME_AWAY`, and `FIXED_AWAY_HOME` + orientations. This period is ignored in orientation transforms + involving one of these orientations and keeps its original + attacking direction. + """ + # change when possession changes BALL_OWNING_TEAM = "ball-owning-team" @@ -304,74 +302,109 @@ class Orientation(Enum): # Not set in dataset NOT_SET = "not-set" - def get_attacking_direction(self, period: Period) -> AttackingDirection: - if self == Orientation.FIXED_HOME_AWAY: - return AttackingDirection.HOME_AWAY - if self == Orientation.FIXED_AWAY_HOME: - return AttackingDirection.AWAY_HOME - if self == Orientation.HOME_AWAY: - dirmap = { - 1: AttackingDirection.HOME_AWAY, - 2: AttackingDirection.AWAY_HOME, - 3: AttackingDirection.HOME_AWAY, - 4: AttackingDirection.AWAY_HOME, - } - return dirmap.get(period.id, period.attacking_direction) - if self == Orientation.AWAY_HOME: + def __repr__(self): + return self.value + + +class AttackingDirection(Enum): + """ + AttackingDirection + + Attributes: + LTR (AttackingDirection): Home team is playing from left to right + RTL (AttackingDirection): Home team is playing from right to left + NOT_SET (AttackingDirection): not set yet + """ + + LTR = "left-to-right" + RTL = "right-to-left" + NOT_SET = "not-set" + + @staticmethod + def from_orientation( + orientation: Orientation, + period: Optional[Period] = None, + ball_owning_team: Optional[Team] = None, + action_executing_team: Optional[Team] = None, + ) -> "AttackingDirection": + """Determines the attacking direction for a specific data record. + + Args: + orientation: The orientation of the dataset. + period: The period of the data record. + ball_owning_team: The team that is in possession of the ball. + action_executing_team: The team that executes the action. + + Raises: + OrientationError: If the attacking direction cannot be determined + from the given data. + + Returns: + The attacking direction for the given data record. + """ + if orientation == Orientation.FIXED_HOME_AWAY: + return AttackingDirection.LTR + if orientation == Orientation.FIXED_AWAY_HOME: + return AttackingDirection.RTL + if orientation == Orientation.HOME_AWAY: + if period is None: + raise OrientationError( + "You must provide a period to determine the attacking direction" + ) dirmap = { - 1: AttackingDirection.AWAY_HOME, - 2: AttackingDirection.HOME_AWAY, - 3: AttackingDirection.AWAY_HOME, - 4: AttackingDirection.HOME_AWAY, + 1: AttackingDirection.LTR, + 2: AttackingDirection.RTL, + 3: AttackingDirection.LTR, + 4: AttackingDirection.RTL, } - return dirmap.get(period.id, period.attacking_direction) - return AttackingDirection.NOT_SET - - def get_orientation_factor( - self, - period: Period, - ball_owning_team: Team, - action_executing_team: Team, - ) -> int: - if period.id == 5: - return 1 # the orientation of penalty shootouts is not transformed - if self == Orientation.FIXED_HOME_AWAY: - return 1 - if self == Orientation.FIXED_AWAY_HOME: - return -1 - if self == Orientation.HOME_AWAY: - if period.id == 1 or period.id == 3: - return 1 - if period.id == 2 or period.id == 4: - return -1 + if period.id in dirmap: + return dirmap[period.id] raise OrientationError( - f"AttackingDirection not defined for period with id {period.id}" + "This orientation is not defined for period %s" % period.id ) - if self == Orientation.AWAY_HOME: - if period.id == 1 or period.id == 3: - return -1 - if period.id == 2 or period.id == 4: - return 1 - raise OrientationError( - f"AttackingDirection not defined for period with id {period.id}" - ) - if self == Orientation.BALL_OWNING_TEAM: - if ball_owning_team.ground == Ground.HOME: - return 1 - if ball_owning_team.ground == Ground.AWAY: - return -1 + if orientation == Orientation.AWAY_HOME: + if period is None: + raise OrientationError( + "You must provide a period to determine the attacking direction" + ) + dirmap = { + 1: AttackingDirection.RTL, + 2: AttackingDirection.LTR, + 3: AttackingDirection.RTL, + 4: AttackingDirection.LTR, + } + if period.id in dirmap: + return dirmap[period.id] raise OrientationError( - f"Invalid ball_owning_team: {ball_owning_team}" + "This orientation is not defined for period %s" % period.id ) - if self == Orientation.ACTION_EXECUTING_TEAM: + if orientation == Orientation.BALL_OWNING_TEAM: + if ball_owning_team is None: + raise OrientationError( + "You must provide the ball owning team to determine the attacking direction" + ) + if ball_owning_team is not None: + if ball_owning_team.ground == Ground.HOME: + return AttackingDirection.LTR + if ball_owning_team.ground == Ground.AWAY: + return AttackingDirection.RTL + raise OrientationError( + "Invalid ball_owning_team: %s", ball_owning_team + ) + return AttackingDirection.NOT_SET + if orientation == Orientation.ACTION_EXECUTING_TEAM: + if action_executing_team is None: + raise ValueError( + "You must provide the action executing team to determine the attacking direction" + ) if action_executing_team.ground == Ground.HOME: - return 1 + return AttackingDirection.LTR if action_executing_team.ground == Ground.AWAY: - return -1 + return AttackingDirection.RTL raise OrientationError( - f"Invalid action_executing_team: {action_executing_team}" + "Invalid action_executing_team: %s", action_executing_team ) - raise OrientationError(f"Unknown orientation: {self}") + raise OrientationError("Unknown orientation: %s", orientation) def __repr__(self): return self.value @@ -811,6 +844,23 @@ def set_refs( self.prev_record = prev self.next_record = next_ + @property + def attacking_direction(self): + if ( + self.dataset + and self.dataset.metadata + and self.dataset.metadata.orientation is not None + ): + try: + return AttackingDirection.from_orientation( + self.dataset.metadata.orientation, + period=self.period, + ball_owning_team=self.ball_owning_team, + ) + except OrientationError: + return AttackingDirection.NOT_SET + return AttackingDirection.NOT_SET + def matches(self, filter_) -> bool: if filter_ is None: return True @@ -876,13 +926,6 @@ def __post_init__(self): # set the pitch dimensions from the coordinate system self.pitch_dimensions = self.coordinate_system.pitch_dimensions - if self.orientation is not None: - # set the attacking directions from the orientation - for period in self.periods: - period.attacking_direction = ( - self.orientation.get_attacking_direction(period) - ) - T = TypeVar("T", bound="DataRecord") diff --git a/kloppy/domain/models/event.py b/kloppy/domain/models/event.py index c40147d3..b25f0d08 100644 --- a/kloppy/domain/models/event.py +++ b/kloppy/domain/models/event.py @@ -12,7 +12,11 @@ TYPE_CHECKING, ) -from kloppy.domain.models.common import DatasetType +from kloppy.domain.models.common import ( + DatasetType, + AttackingDirection, + OrientationError, +) from kloppy.utils import ( camelcase_to_snakecase, removes_suffix, @@ -530,6 +534,24 @@ def event_type(self) -> EventType: def event_name(self) -> str: raise NotImplementedError + @property + def attacking_direction(self): + if ( + self.dataset + and self.dataset.metadata + and self.dataset.metadata.orientation is not None + ): + try: + return AttackingDirection.from_orientation( + self.dataset.metadata.orientation, + period=self.period, + ball_owning_team=self.ball_owning_team, + action_executing_team=self.team, + ) + except OrientationError: + return AttackingDirection.NOT_SET + return AttackingDirection.NOT_SET + def get_qualifier_value(self, qualifier_type: Type[Qualifier]): """ Returns the Qualifier of a certain type, or None if qualifier is not present. diff --git a/kloppy/domain/services/__init__.py b/kloppy/domain/services/__init__.py index 404a7f8c..d72375cc 100644 --- a/kloppy/domain/services/__init__.py +++ b/kloppy/domain/services/__init__.py @@ -15,7 +15,7 @@ def avg(items: List[float]) -> float: def attacking_direction_from_frame(frame: Frame) -> AttackingDirection: - """This method should only be called for the first frame of a""" + """This method should only be called for the first frame of a period.""" avg_x_home = avg( [ player_data.coordinates.x @@ -32,6 +32,6 @@ def attacking_direction_from_frame(frame: Frame) -> AttackingDirection: ) if avg_x_home < avg_x_away: - return AttackingDirection.HOME_AWAY + return AttackingDirection.LTR else: - return AttackingDirection.AWAY_HOME + return AttackingDirection.RTL diff --git a/kloppy/domain/services/transformers/dataset.py b/kloppy/domain/services/transformers/dataset.py index e9507937..11f44b52 100644 --- a/kloppy/domain/services/transformers/dataset.py +++ b/kloppy/domain/services/transformers/dataset.py @@ -149,21 +149,22 @@ def __needs_flip( if action_executing_team is None: action_executing_team = ball_owning_team - orientation_factor_from = ( - self._from_orientation.get_orientation_factor( - ball_owning_team=ball_owning_team, - period=period, - action_executing_team=action_executing_team, - ) + attacking_direction_from = AttackingDirection.from_orientation( + self._from_orientation, + period=period, + ball_owning_team=ball_owning_team, + action_executing_team=action_executing_team, ) - orientation_factor_to = ( - self._to_orientation.get_orientation_factor( - ball_owning_team=ball_owning_team, - period=period, - action_executing_team=action_executing_team, - ) + attacking_direction_to = AttackingDirection.from_orientation( + self._to_orientation, + period=period, + ball_owning_team=ball_owning_team, + action_executing_team=action_executing_team, + ) + flip = ( + attacking_direction_from != attacking_direction_to + and attacking_direction_to != AttackingDirection.NOT_SET ) - flip = orientation_factor_from != orientation_factor_to return flip def transform_frame(self, frame: Frame) -> Frame: @@ -183,8 +184,6 @@ def transform_frame(self, frame: Frame) -> Frame: ): frame = self.__flip_frame(frame) - frame = self.__change_attacking_direction(frame) - return frame def __change_frame_coordinate_system(self, frame: Frame): @@ -264,15 +263,6 @@ def __change_point_coordinate_system( else: return Point(x=x, y=y) - def __change_attacking_direction(self, record: DataRecord): - new_attacking_direction = self._to_orientation.get_attacking_direction( - record.period - ) - period = replace( - record.period, attacking_direction=new_attacking_direction - ) - return replace(record, period=period) - def __flip_frame(self, frame: Frame): players_data = {} for player, data in frame.players_data.items(): @@ -317,8 +307,6 @@ def transform_event(self, event: Event) -> Event: if event.freeze_frame: event.freeze_frame = self.transform_frame(event.freeze_frame) - event = self.__change_attacking_direction(event) - return event def __change_event_coordinate_system(self, event: Event): diff --git a/kloppy/infra/serializers/event/datafactory/deserializer.py b/kloppy/infra/serializers/event/datafactory/deserializer.py index 2bac39c0..99d8d289 100644 --- a/kloppy/infra/serializers/event/datafactory/deserializer.py +++ b/kloppy/infra/serializers/event/datafactory/deserializer.py @@ -416,9 +416,6 @@ def deserialize(self, inputs: DatafactoryInputs) -> EventDataset: id=half, start_timestamp=start_ts[half], end_timestamp=end_ts, - attacking_direction=AttackingDirection.HOME_AWAY - if half % 2 == 1 - else AttackingDirection.AWAY_HOME, ) # exclude goals, already listed as shots too @@ -576,7 +573,7 @@ def deserialize(self, inputs: DatafactoryInputs) -> EventDataset: periods=sorted(periods.values(), key=lambda p: p.id), pitch_dimensions=transformer.get_to_coordinate_system().pitch_dimensions, frame_rate=None, - orientation=Orientation.HOME_TEAM, + orientation=Orientation.HOME_AWAY, flags=DatasetFlag.BALL_OWNING_TEAM, score=score, provider=Provider.DATAFACTORY, diff --git a/kloppy/infra/serializers/event/sportec/deserializer.py b/kloppy/infra/serializers/event/sportec/deserializer.py index fa29f753..2507484a 100644 --- a/kloppy/infra/serializers/event/sportec/deserializer.py +++ b/kloppy/infra/serializers/event/sportec/deserializer.py @@ -112,7 +112,10 @@ def sportec_metadata_from_xml_elm(match_root) -> SportecMetadata: if not away_team: raise DeserializationError("Away team is missing from metadata") - (home_score, away_score,) = match_root.MatchInformation.General.attrib[ + ( + home_score, + away_score, + ) = match_root.MatchInformation.General.attrib[ "Result" ].split(":") score = Score(home=int(home_score), away=int(away_score)) @@ -412,25 +415,10 @@ def deserialize(self, inputs: SportecEventDataInputs) -> EventDataset: team_left = event_chain[SPORTEC_EVENT_NAME_KICKOFF][ "TeamLeft" ] - if team_left == home_team.team_id: - # goal of home team is on the left side. - # this means they attack from left to right - orientation = Orientation.FIXED_HOME_AWAY - period.set_attacking_direction( - AttackingDirection.HOME_AWAY - ) - else: - orientation = Orientation.FIXED_AWAY_HOME - period.set_attacking_direction( - AttackingDirection.AWAY_HOME - ) - else: - last_period = periods[-1] - period.set_attacking_direction( - AttackingDirection.AWAY_HOME - if last_period.attacking_direction - == AttackingDirection.HOME_AWAY - else AttackingDirection.HOME_AWAY + orientation = ( + Orientation.HOME_AWAY + if team_left == home_team.team_id + else Orientation.AWAY_HOME ) periods.append(period) diff --git a/kloppy/infra/serializers/event/statsbomb/deserializer.py b/kloppy/infra/serializers/event/statsbomb/deserializer.py index 419e96ac..920774a0 100644 --- a/kloppy/infra/serializers/event/statsbomb/deserializer.py +++ b/kloppy/infra/serializers/event/statsbomb/deserializer.py @@ -228,7 +228,7 @@ def create_periods(self, raw_events): ::2 ] # recorded for each team, take every other periods = [] - for (start_event, end_event) in zip_longest( + for start_event, end_event in zip_longest( half_start_and_end_events[::2], half_start_and_end_events[1::2] ): if ( diff --git a/kloppy/infra/serializers/tracking/metrica_csv.py b/kloppy/infra/serializers/tracking/metrica_csv.py index fa61690c..163c8d30 100644 --- a/kloppy/infra/serializers/tracking/metrica_csv.py +++ b/kloppy/infra/serializers/tracking/metrica_csv.py @@ -1,4 +1,5 @@ import logging +import warnings from collections import namedtuple from typing import Tuple, Dict, Iterator, IO, NamedTuple @@ -204,13 +205,6 @@ def deserialize( if not periods or period.id != periods[-1].id: periods.append(period) - if not period.attacking_direction_set: - period.set_attacking_direction( - attacking_direction=attacking_direction_from_frame( - frame - ) - ) - if n == 0: teams = [home_partial_frame.team, away_partial_frame.team] @@ -218,11 +212,21 @@ def deserialize( if self.limit and n >= self.limit: break - orientation = ( - Orientation.FIXED_HOME_AWAY - if periods[0].attacking_direction == AttackingDirection.HOME_AWAY - else Orientation.FIXED_AWAY_HOME - ) + try: + first_frame = next( + frame for frame in frames if frame.period.id == 1 + ) + orientation = ( + Orientation.HOME_AWAY + if attacking_direction_from_frame(first_frame) + == AttackingDirection.LTR + else Orientation.AWAY_HOME + ) + except StopIteration: + warnings.warn( + "Could not determine orientation of dataset, defaulting to NOT_SET" + ) + orientation = Orientation.NOT_SET metadata = Metadata( teams=teams, diff --git a/kloppy/infra/serializers/tracking/metrica_epts/metadata.py b/kloppy/infra/serializers/tracking/metrica_epts/metadata.py index f87ffc46..4200fbe2 100644 --- a/kloppy/infra/serializers/tracking/metrica_epts/metadata.py +++ b/kloppy/infra/serializers/tracking/metrica_epts/metadata.py @@ -62,33 +62,26 @@ def _load_periods( ] periods = [] + start_attacking_direction = AttackingDirection.NOT_SET for idx, period_name in enumerate(period_names): # the attacking direction is only defined for the first period # and alternates between periods - invert = idx % 2 - if ( - provider_teams_params[Ground.HOME].get( - "attack_direction_first_half" - ) - == "left_to_right" - ): - attacking_direction = [ - AttackingDirection.HOME_AWAY, - AttackingDirection.AWAY_HOME, - ][invert] - elif ( - provider_teams_params[Ground.HOME].get( - "attack_direction_first_half" - ) - == "right_to_left" - ): - attacking_direction = [ - AttackingDirection.AWAY_HOME, - AttackingDirection.HOME_AWAY, - ][invert] - else: - attacking_direction = AttackingDirection.NOT_SET + if idx == 0: + if ( + provider_teams_params[Ground.HOME].get( + "attack_direction_first_half" + ) + == "left_to_right" + ): + start_attacking_direction = AttackingDirection.LTR + elif ( + provider_teams_params[Ground.HOME].get( + "attack_direction_first_half" + ) + == "right_to_left" + ): + start_attacking_direction = AttackingDirection.RTL start_key = f"{period_name}_start" end_key = f"{period_name}_end" @@ -99,14 +92,13 @@ def _load_periods( start_timestamp=float(provider_params[start_key]) / frame_rate, end_timestamp=float(provider_params[end_key]) / frame_rate, - attacking_direction=attacking_direction, ) ) else: # done break - return periods + return periods, start_attacking_direction def _load_players(players_elm, team: Team) -> List[Player]: @@ -293,24 +285,21 @@ def load_metadata( frame_rate = int(metadata.find("GlobalConfig").find("FrameRate")) pitch_dimensions = _load_pitch_dimensions(metadata, sensors) - periods = _load_periods(metadata, _team_map, frame_rate) + periods, start_attacking_direction = _load_periods( + metadata, _team_map, frame_rate + ) - if periods: - start_attacking_direction = periods[0].attacking_direction + if start_attacking_direction != AttackingDirection.NOT_SET: + orientation = ( + Orientation.HOME_AWAY + if start_attacking_direction == AttackingDirection.LTR + else Orientation.AWAY_HOME + ) else: - start_attacking_direction = None - - orientation = ( - ( - Orientation.HOME_TEAM - if start_attacking_direction == AttackingDirection.HOME_AWAY - else Orientation.AWAY_TEAM + warnings.warn( + "Could not determine orientation of dataset, defaulting to NOT_SET" ) - if start_attacking_direction != AttackingDirection.NOT_SET - else Orientation.NOT_SET - ) - - metadata.orientation = orientation + orientation = Orientation.NOT_SET if provider and pitch_dimensions: from_coordinate_system = build_coordinate_system( diff --git a/kloppy/infra/serializers/tracking/secondspectrum.py b/kloppy/infra/serializers/tracking/secondspectrum.py index e1f0dcb0..a5dfb1a0 100644 --- a/kloppy/infra/serializers/tracking/secondspectrum.py +++ b/kloppy/infra/serializers/tracking/secondspectrum.py @@ -1,5 +1,6 @@ import json import logging +import warnings from typing import Tuple, Dict, Optional, Union, NamedTuple, IO from lxml import objectify @@ -261,21 +262,24 @@ def _iter(): frame = transformer.transform_frame(frame) frames.append(frame) - if not period.attacking_direction_set: - period.set_attacking_direction( - attacking_direction=attacking_direction_from_frame( - frame - ) - ) - if self.limit and n + 1 >= self.limit: break - orientation = ( - Orientation.FIXED_HOME_AWAY - if periods[0].attacking_direction == AttackingDirection.HOME_AWAY - else Orientation.FIXED_AWAY_HOME - ) + try: + first_frame = next( + frame for frame in frames if frame.period.id == 1 + ) + orientation = ( + Orientation.HOME_AWAY + if attacking_direction_from_frame(first_frame) + == AttackingDirection.LTR + else Orientation.AWAY_HOME + ) + except StopIteration: + warnings.warn( + "Could not determine orientation of dataset, defaulting to NOT_SET" + ) + orientation = Orientation.NOT_SET metadata = Metadata( teams=teams, diff --git a/kloppy/infra/serializers/tracking/skillcorner.py b/kloppy/infra/serializers/tracking/skillcorner.py index ebdbe1e8..829019aa 100644 --- a/kloppy/infra/serializers/tracking/skillcorner.py +++ b/kloppy/infra/serializers/tracking/skillcorner.py @@ -1,4 +1,5 @@ import logging +import warnings from typing import List, Dict, Tuple, NamedTuple, IO, Optional, Union from enum import Enum, Flag from collections import Counter @@ -151,35 +152,33 @@ def _timestamp_from_timestring(cls, timestring): return 60 * float(m) + float(s) @classmethod - def _set_skillcorner_attacking_directions(cls, frames, periods): + def _get_skillcorner_attacking_directions(cls, frames, periods): """ with only partial tracking data we cannot rely on a single frame to infer the attacking directions as a simple average of only some players x-coords might not reflect the attacking direction. """ - attacking_directions = [] - - for frame in frames: - if len(frame.players_data) > 0: - attacking_directions.append( - attacking_direction_from_frame(frame) - ) - else: - attacking_directions.append(AttackingDirection.NOT_SET) - - frame_periods = np.array([_frame.period.id for _frame in frames]) + attacking_directions = {} + frame_period_ids = np.array([_frame.period.id for _frame in frames]) + frame_attacking_directions = np.array( + [ + attacking_direction_from_frame(frame) + if len(frame.players_data) > 0 + else AttackingDirection.NOT_SET + for frame in frames + ] + ) - for period in periods.keys(): - if period in frame_periods: + for period_id in periods.keys(): + if period_id in frame_period_ids: count = Counter( - np.array(attacking_directions)[frame_periods == period] + frame_attacking_directions[frame_period_ids == period_id] ) - att_direction = count.most_common()[0][0] - periods[period].attacking_direction = att_direction + attacking_directions[period_id] = count.most_common()[0][0] else: - periods[ - period - ].attacking_direction = AttackingDirection.NOT_SET + attacking_directions[period_id] = AttackingDirection.NOT_SET + + return attacking_directions def __load_json(self, file): return json.load(file) @@ -376,15 +375,18 @@ def _iter(): if self.limit and n_frames >= self.limit: break - self._set_skillcorner_attacking_directions(frames, periods) - - frame_rate = 10 - - orientation = ( - Orientation.HOME_TEAM - if periods[1].attacking_direction == AttackingDirection.HOME_AWAY - else Orientation.AWAY_TEAM + attacking_directions = self._get_skillcorner_attacking_directions( + frames, periods ) + if attacking_directions[1] == AttackingDirection.LTR: + orientation = Orientation.HOME_AWAY + elif attacking_directions[1] == AttackingDirection.RTL: + orientation = Orientation.AWAY_HOME + else: + warnings.warn( + "Could not determine orientation of dataset, defaulting to NOT_SET" + ) + orientation = Orientation.NOT_SET metadata = Metadata( teams=teams, @@ -394,7 +396,7 @@ def _iter(): home=metadata["home_team_score"], away=metadata["away_team_score"], ), - frame_rate=frame_rate, + frame_rate=10, orientation=orientation, provider=Provider.SKILLCORNER, flags=~(DatasetFlag.BALL_STATE | DatasetFlag.BALL_OWNING_TEAM), diff --git a/kloppy/infra/serializers/tracking/sportec/deserializer.py b/kloppy/infra/serializers/tracking/sportec/deserializer.py index 45b05b1f..3458edfd 100644 --- a/kloppy/infra/serializers/tracking/sportec/deserializer.py +++ b/kloppy/infra/serializers/tracking/sportec/deserializer.py @@ -1,4 +1,5 @@ import logging +import warnings from collections import defaultdict from typing import NamedTuple, Optional, Union, IO @@ -195,24 +196,26 @@ def _iter(): frames = [] for n, frame in enumerate(_iter()): frame = transformer.transform_frame(frame) - frames.append(frame) - if not frame.period.attacking_direction_set: - frame.period.set_attacking_direction( - attacking_direction=attacking_direction_from_frame( - frame - ) - ) - if self.limit and n >= self.limit: break - orientation = ( - Orientation.FIXED_HOME_AWAY - if periods[0].attacking_direction == AttackingDirection.HOME_AWAY - else Orientation.FIXED_AWAY_HOME - ) + try: + first_frame = next( + frame for frame in frames if frame.period.id == 1 + ) + orientation = ( + Orientation.HOME_AWAY + if attacking_direction_from_frame(first_frame) + == AttackingDirection.LTR + else Orientation.AWAY_HOME + ) + except StopIteration: + warnings.warn( + "Could not determine orientation of dataset, defaulting to NOT_SET" + ) + orientation = Orientation.NOT_SET metadata = Metadata( teams=teams, diff --git a/kloppy/infra/serializers/tracking/statsperform.py b/kloppy/infra/serializers/tracking/statsperform.py index 6c5d6687..dada36da 100644 --- a/kloppy/infra/serializers/tracking/statsperform.py +++ b/kloppy/infra/serializers/tracking/statsperform.py @@ -1,5 +1,6 @@ import json import logging +import warnings from typing import IO, Any, Dict, List, NamedTuple, Optional, Union from lxml import objectify @@ -309,21 +310,24 @@ def _iter(): frame = transformer.transform_frame(frame) frames.append(frame) - if not period.attacking_direction_set: - period.set_attacking_direction( - attacking_direction=attacking_direction_from_frame( - frame - ) - ) - if self.limit and n >= self.limit: break - orientation = ( - Orientation.FIXED_HOME_AWAY - if periods[1].attacking_direction == AttackingDirection.HOME_AWAY - else Orientation.FIXED_AWAY_HOME - ) + try: + first_frame = next( + frame for frame in frames if frame.period.id == 1 + ) + orientation = ( + Orientation.HOME_AWAY + if attacking_direction_from_frame(first_frame) + == AttackingDirection.LTR + else Orientation.AWAY_HOME + ) + except StopIteration: + warnings.warn( + "Could not determine orientation of dataset, defaulting to NOT_SET" + ) + orientation = Orientation.NOT_SET meta_data = Metadata( teams=teams_list, diff --git a/kloppy/infra/serializers/tracking/tracab.py b/kloppy/infra/serializers/tracking/tracab.py index f7a0e162..f9cd7729 100644 --- a/kloppy/infra/serializers/tracking/tracab.py +++ b/kloppy/infra/serializers/tracking/tracab.py @@ -1,4 +1,5 @@ import logging +import warnings from typing import Tuple, Dict, NamedTuple, IO, Optional, Union from lxml import objectify @@ -184,24 +185,26 @@ def _iter(): frame = self._frame_from_line(teams, period, line, frame_rate) frame = transformer.transform_frame(frame) - frames.append(frame) - if not period.attacking_direction_set: - period.set_attacking_direction( - attacking_direction=attacking_direction_from_frame( - frame - ) - ) - if self.limit and n >= self.limit: break - orientation = ( - Orientation.FIXED_HOME_AWAY - if periods[0].attacking_direction == AttackingDirection.HOME_AWAY - else Orientation.FIXED_AWAY_HOME - ) + try: + first_frame = next( + frame for frame in frames if frame.period.id == 1 + ) + orientation = ( + Orientation.HOME_AWAY + if attacking_direction_from_frame(first_frame) + == AttackingDirection.LTR + else Orientation.AWAY_HOME + ) + except StopIteration: + warnings.warn( + "Could not determine orientation of dataset, defaulting to NOT_SET" + ) + orientation = Orientation.NOT_SET metadata = Metadata( teams=teams, diff --git a/kloppy/tests/test_datafactory.py b/kloppy/tests/test_datafactory.py index ba2c029e..731a5aab 100644 --- a/kloppy/tests/test_datafactory.py +++ b/kloppy/tests/test_datafactory.py @@ -30,7 +30,7 @@ def test_correct_deserialization(self, event_data: str): assert len(dataset.metadata.periods) == 2 assert dataset.events[10].ball_owning_team == dataset.metadata.teams[1] assert dataset.events[23].ball_owning_team == dataset.metadata.teams[0] - assert dataset.metadata.orientation == Orientation.HOME_TEAM + assert dataset.metadata.orientation == Orientation.HOME_AWAY assert dataset.metadata.teams[0].name == "Team A" assert dataset.metadata.teams[0].ground == Ground.HOME assert dataset.metadata.teams[1].name == "Team B" @@ -47,13 +47,11 @@ def test_correct_deserialization(self, event_data: str): id=1, start_timestamp=0, end_timestamp=2912, - attacking_direction=AttackingDirection.HOME_AWAY, ) assert dataset.metadata.periods[1] == Period( id=2, start_timestamp=2700, end_timestamp=5710, - attacking_direction=AttackingDirection.AWAY_HOME, ) assert dataset.events[0].coordinates == Point(0.01, 0.01) diff --git a/kloppy/tests/test_helpers.py b/kloppy/tests/test_helpers.py index b40b9e47..1922aef0 100644 --- a/kloppy/tests/test_helpers.py +++ b/kloppy/tests/test_helpers.py @@ -44,13 +44,11 @@ def _get_tracking_dataset(self): id=1, start_timestamp=0.0, end_timestamp=10.0, - attacking_direction=AttackingDirection.HOME_AWAY, ), Period( id=2, start_timestamp=15.0, end_timestamp=25.0, - attacking_direction=AttackingDirection.AWAY_HOME, ), ] metadata = Metadata( @@ -161,23 +159,8 @@ def test_transform_to_orientation(self): assert original.frames[0].ball_coordinates == Point3D(x=1, y=0, z=0) assert original.frames[1].ball_coordinates == Point3D(x=0, y=1, z=1) # the frames should have the correct attacking direction - assert ( - original.frames[0].period.attacking_direction - == AttackingDirection.HOME_AWAY - ) - assert ( - original.frames[1].period.attacking_direction - == AttackingDirection.AWAY_HOME - ) - # the metadata should have the correct attacking direction - assert ( - original.metadata.periods[0].attacking_direction - == AttackingDirection.HOME_AWAY - ) - assert ( - original.metadata.periods[1].attacking_direction - == AttackingDirection.AWAY_HOME - ) + assert original.frames[0].attacking_direction == AttackingDirection.LTR + assert original.frames[1].attacking_direction == AttackingDirection.RTL # Transform to AWAY_HOME orientation transform1 = original.transform( @@ -190,21 +173,10 @@ def test_transform_to_orientation(self): assert transform1.frames[1].ball_coordinates == Point3D(x=1, y=0, z=1) # the frames should have the correct attacking direction assert ( - transform1.frames[0].period.attacking_direction - == AttackingDirection.AWAY_HOME - ) - assert ( - transform1.frames[1].period.attacking_direction - == AttackingDirection.HOME_AWAY - ) - # the metadata should have the correct attacking direction - assert ( - transform1.metadata.periods[0].attacking_direction - == AttackingDirection.AWAY_HOME + transform1.frames[0].attacking_direction == AttackingDirection.RTL ) assert ( - transform1.metadata.periods[1].attacking_direction - == AttackingDirection.HOME_AWAY + transform1.frames[1].attacking_direction == AttackingDirection.LTR ) # Transform to FIXED_AWAY_HOME orientation @@ -218,13 +190,7 @@ def test_transform_to_orientation(self): assert transform2.frames[1].ball_coordinates == Point3D(x=0, y=1, z=1) # the frames should have the correct attacking direction for frame in transform2.frames: - assert ( - frame.period.attacking_direction - == AttackingDirection.AWAY_HOME - ) - # the metadata should have the correct attacking direction - for period in transform2.metadata.periods: - assert period.attacking_direction == AttackingDirection.AWAY_HOME + assert frame.attacking_direction == AttackingDirection.RTL # Transform to BALL_OWNING_TEAM orientation transform3 = transform2.transform( @@ -236,15 +202,15 @@ def test_transform_to_orientation(self): assert transform3.frames[0].ball_coordinates == Point3D(x=1, y=0, z=0) assert transform3.frames[1].ball_coordinates == Point3D(x=0, y=1, z=1) # the frames should have the correct attacking direction - for frame in transform3.frames: - assert ( - frame.period.attacking_direction == AttackingDirection.NOT_SET - ) - # the metadata should have the correct attacking direction - for period in transform3.metadata.periods: - assert period.attacking_direction == AttackingDirection.NOT_SET + assert ( + transform3.frames[0].attacking_direction == AttackingDirection.LTR + ) + assert ( + transform3.frames[1].attacking_direction == AttackingDirection.RTL + ) # Transform to ACTION_EXECUTING_TEAM orientation + # this should be identical to BALL_OWNING_TEAM for tracking data transform4 = transform3.transform( to_orientation=Orientation.ACTION_EXECUTING_TEAM, to_pitch_dimensions=[[0, 1], [0, 1]], @@ -253,17 +219,10 @@ def test_transform_to_orientation(self): transform4.metadata.orientation == Orientation.ACTION_EXECUTING_TEAM ) - # should be identical to transform3 as the action_executing team is not defined - assert transform4.frames[0].ball_coordinates == Point3D(x=1, y=0, z=0) - # the frames should have the correct attacking direction assert transform4.frames[1].ball_coordinates == Point3D(x=0, y=1, z=1) - for frame in transform4.frames: - assert ( - frame.period.attacking_direction == AttackingDirection.NOT_SET - ) - # the metadata should have the correct attacking direction - for period in transform4.metadata.periods: - assert period.attacking_direction == AttackingDirection.NOT_SET + for frame_t3, frame_t4 in zip(transform3.frames, transform4.frames): + assert frame_t3.ball_coordinates == frame_t4.ball_coordinates + assert frame_t3.attacking_direction == frame_t4.attacking_direction # Transform back to the original HOME_AWAY orientation transform5 = transform4.transform( @@ -273,18 +232,9 @@ def test_transform_to_orientation(self): # we should be back at the original for frame1, frame2 in zip(original.frames, transform5.frames): assert frame1.ball_coordinates == frame2.ball_coordinates - assert ( - frame1.period.attacking_direction - == frame2.period.attacking_direction - ) - for period1, period2 in zip( - original.metadata.periods, transform5.metadata.periods - ): - assert period1.attacking_direction == period2.attacking_direction - - def test_transform_to_coordinate_system(self): - base_dir = os.path.dirname(__file__) + assert frame1.attacking_direction == frame2.attacking_direction + def test_transform_to_coordinate_system(self, base_dir): dataset = tracab.load( meta_data=base_dir / "files/tracab_meta.xml", raw_data=base_dir / "files/tracab_raw.dat", diff --git a/kloppy/tests/test_metrica_csv.py b/kloppy/tests/test_metrica_csv.py index fe4c741a..57ce3495 100644 --- a/kloppy/tests/test_metrica_csv.py +++ b/kloppy/tests/test_metrica_csv.py @@ -31,18 +31,16 @@ def test_correct_deserialization(self, home_data: str, away_data: str): assert dataset.dataset_type == DatasetType.TRACKING assert len(dataset.records) == 6 assert len(dataset.metadata.periods) == 2 - assert dataset.metadata.orientation == Orientation.FIXED_HOME_AWAY + assert dataset.metadata.orientation == Orientation.HOME_AWAY assert dataset.metadata.periods[0] == Period( id=1, start_timestamp=0.04, end_timestamp=0.12, - attacking_direction=AttackingDirection.HOME_AWAY, ) assert dataset.metadata.periods[1] == Period( id=2, start_timestamp=5800.16, end_timestamp=5800.24, - attacking_direction=AttackingDirection.AWAY_HOME, ) # make sure data is loaded correctly (including flip y-axis) diff --git a/kloppy/tests/test_metrica_epts.py b/kloppy/tests/test_metrica_epts.py index f1696460..e626be2c 100644 --- a/kloppy/tests/test_metrica_epts.py +++ b/kloppy/tests/test_metrica_epts.py @@ -115,7 +115,7 @@ def test_correct_deserialization(self, meta_data: str, raw_data: str): assert len(dataset.records) == 100 assert len(dataset.metadata.periods) == 2 - assert dataset.metadata.orientation is Orientation.HOME_TEAM + assert dataset.metadata.orientation is Orientation.HOME_AWAY assert dataset.records[0].players_data[ first_player diff --git a/kloppy/tests/test_metrica_events.py b/kloppy/tests/test_metrica_events.py index 08d2d0f3..fef37456 100644 --- a/kloppy/tests/test_metrica_events.py +++ b/kloppy/tests/test_metrica_events.py @@ -36,7 +36,7 @@ def test_metadata(self, dataset: EventDataset): """It should parse the metadata correctly.""" assert dataset.metadata.provider == Provider.METRICA assert len(dataset.metadata.periods) == 2 - assert dataset.metadata.orientation is Orientation.HOME_TEAM + assert dataset.metadata.orientation is Orientation.HOME_AWAY assert dataset.metadata.teams[0].name == "Team A" assert dataset.metadata.teams[1].name == "Team B" player = dataset.metadata.teams[0].players[10] @@ -49,13 +49,11 @@ def test_metadata(self, dataset: EventDataset): id=1, start_timestamp=14.44, end_timestamp=2783.76, - attacking_direction=AttackingDirection.NOT_SET, ) assert dataset.metadata.periods[1] == Period( id=2, start_timestamp=2803.6, end_timestamp=5742.12, - attacking_direction=AttackingDirection.NOT_SET, ) def test_coordinates(self, dataset: EventDataset): diff --git a/kloppy/tests/test_opta.py b/kloppy/tests/test_opta.py index 59261a13..96f74852 100644 --- a/kloppy/tests/test_opta.py +++ b/kloppy/tests/test_opta.py @@ -89,19 +89,16 @@ def test_correct_deserialization(self, f7_data: str, f24_data: str): id=1, start_timestamp=1537714933.608, end_timestamp=1537717701.222, - attacking_direction=AttackingDirection.NOT_SET, ) assert dataset.metadata.periods[1] == Period( id=2, start_timestamp=1537718728.873, end_timestamp=1537721737.788, - attacking_direction=AttackingDirection.NOT_SET, ) assert dataset.metadata.periods[4] == Period( id=5, start_timestamp=1537729501.81, end_timestamp=1537730701.81, - attacking_direction=AttackingDirection.NOT_SET, ) assert dataset.events[0].coordinates == Point(50.1, 49.4) diff --git a/kloppy/tests/test_secondspectrum.py b/kloppy/tests/test_secondspectrum.py index f556bff9..b7116232 100644 --- a/kloppy/tests/test_secondspectrum.py +++ b/kloppy/tests/test_secondspectrum.py @@ -44,24 +44,16 @@ def test_correct_deserialization( assert dataset.dataset_type == DatasetType.TRACKING assert len(dataset.records) == 376 assert len(dataset.metadata.periods) == 2 - assert dataset.metadata.orientation == Orientation.FIXED_AWAY_HOME + assert dataset.metadata.orientation == Orientation.AWAY_HOME # Check the Periods assert dataset.metadata.periods[0].id == 1 assert dataset.metadata.periods[0].start_timestamp == 0 assert dataset.metadata.periods[0].end_timestamp == 2982240 - assert ( - dataset.metadata.periods[0].attacking_direction - == AttackingDirection.AWAY_HOME - ) assert dataset.metadata.periods[1].id == 2 assert dataset.metadata.periods[1].start_timestamp == 3907360 assert dataset.metadata.periods[1].end_timestamp == 6927840 - assert ( - dataset.metadata.periods[1].attacking_direction - == AttackingDirection.HOME_AWAY - ) # Check some timestamps assert dataset.records[0].timestamp == 0 # First frame diff --git a/kloppy/tests/test_skillcorner.py b/kloppy/tests/test_skillcorner.py index 67741ad4..b2c234e0 100644 --- a/kloppy/tests/test_skillcorner.py +++ b/kloppy/tests/test_skillcorner.py @@ -33,18 +33,16 @@ def test_correct_deserialization(self, raw_data: Path, meta_data: Path): assert dataset.dataset_type == DatasetType.TRACKING assert len(dataset.records) == 34783 assert len(dataset.metadata.periods) == 2 - assert dataset.metadata.orientation == Orientation.AWAY_TEAM + assert dataset.metadata.orientation == Orientation.AWAY_HOME assert dataset.metadata.periods[0] == Period( id=1, start_timestamp=0.0, end_timestamp=2753.3, - attacking_direction=AttackingDirection.AWAY_HOME, ) assert dataset.metadata.periods[1] == Period( id=2, start_timestamp=2700.0, end_timestamp=5509.7, - attacking_direction=AttackingDirection.HOME_AWAY, ) # are frames with wrong camera views and pregame skipped? diff --git a/kloppy/tests/test_sportec.py b/kloppy/tests/test_sportec.py index d19613eb..e8990538 100644 --- a/kloppy/tests/test_sportec.py +++ b/kloppy/tests/test_sportec.py @@ -49,18 +49,16 @@ def test_correct_event_data_deserialization( assert len(dataset.events) == 29 assert dataset.events[28].result == ShotResult.OWN_GOAL - assert dataset.metadata.orientation == Orientation.FIXED_HOME_AWAY + assert dataset.metadata.orientation == Orientation.HOME_AWAY assert dataset.metadata.periods[0] == Period( id=1, start_timestamp=1591381800.21, end_timestamp=1591384584.0, - attacking_direction=AttackingDirection.HOME_AWAY, ) assert dataset.metadata.periods[1] == Period( id=2, start_timestamp=1591385607.01, end_timestamp=1591388598.0, - attacking_direction=AttackingDirection.AWAY_HOME, ) player = dataset.metadata.teams[0].players[0] diff --git a/kloppy/tests/test_statsbomb.py b/kloppy/tests/test_statsbomb.py index 4115f1f5..a2c6c8ec 100644 --- a/kloppy/tests/test_statsbomb.py +++ b/kloppy/tests/test_statsbomb.py @@ -155,10 +155,6 @@ def test_periods(self, dataset): assert dataset.metadata.periods[0].end_timestamp == parse_str_ts( "00:47:38.122" ) - assert ( - dataset.metadata.periods[0].attacking_direction - == AttackingDirection.NOT_SET - ) assert dataset.metadata.periods[1].id == 2 assert dataset.metadata.periods[1].start_timestamp == parse_str_ts( "00:47:38.122" @@ -166,10 +162,6 @@ def test_periods(self, dataset): assert dataset.metadata.periods[1].end_timestamp == parse_str_ts( "00:47:38.122" ) + parse_str_ts("00:50:29.638") - assert ( - dataset.metadata.periods[1].attacking_direction - == AttackingDirection.NOT_SET - ) def test_pitch_dimensions(self, dataset): """It should set the correct pitch dimensions""" diff --git a/kloppy/tests/test_statsperform.py b/kloppy/tests/test_statsperform.py index 68770e8b..b23f70f3 100644 --- a/kloppy/tests/test_statsperform.py +++ b/kloppy/tests/test_statsperform.py @@ -49,24 +49,16 @@ def test_correct_deserialization(self, meta_data: Path, raw_data: Path): assert dataset.dataset_type == DatasetType.TRACKING assert len(dataset.records) == 92 assert len(dataset.metadata.periods) == 2 - assert dataset.metadata.orientation == Orientation.FIXED_AWAY_HOME + assert dataset.metadata.orientation == Orientation.AWAY_HOME # Check the periods assert dataset.metadata.periods[1].id == 1 assert dataset.metadata.periods[1].start_timestamp == 0 assert dataset.metadata.periods[1].end_timestamp == 2500 - assert ( - dataset.metadata.periods[1].attacking_direction - == AttackingDirection.AWAY_HOME - ) assert dataset.metadata.periods[2].id == 2 assert dataset.metadata.periods[2].start_timestamp == 0 assert dataset.metadata.periods[2].end_timestamp == 6500 - assert ( - dataset.metadata.periods[2].attacking_direction - == AttackingDirection.HOME_AWAY - ) # Check some timestamps assert dataset.records[0].timestamp == 0 # First frame diff --git a/kloppy/tests/test_tracab.py b/kloppy/tests/test_tracab.py index a97d9d8f..100267a5 100644 --- a/kloppy/tests/test_tracab.py +++ b/kloppy/tests/test_tracab.py @@ -39,19 +39,17 @@ def test_correct_deserialization(self, meta_data: Path, raw_data: Path): assert dataset.dataset_type == DatasetType.TRACKING assert len(dataset.records) == 6 assert len(dataset.metadata.periods) == 2 - assert dataset.metadata.orientation == Orientation.FIXED_HOME_AWAY + assert dataset.metadata.orientation == Orientation.HOME_AWAY assert dataset.metadata.periods[0] == Period( id=1, start_timestamp=4.0, end_timestamp=4.08, - attacking_direction=AttackingDirection.HOME_AWAY, ) assert dataset.metadata.periods[1] == Period( id=2, start_timestamp=8.0, end_timestamp=8.08, - attacking_direction=AttackingDirection.AWAY_HOME, ) player_home_19 = dataset.metadata.teams[0].get_player_by_jersey_number( From 3ffe1adf855ccaeb38327f4b456ac91e52115287 Mon Sep 17 00:00:00 2001 From: Pieter Robberechts Date: Sun, 14 Jan 2024 17:39:12 +0100 Subject: [PATCH 5/6] Rename FIXED_ -> STATIC_ --- examples/datasets/statsbomb.py | 2 +- kloppy/domain/models/common.py | 14 +++++++------- kloppy/tests/issues/issue_113/test_issue_113.py | 2 +- kloppy/tests/test_helpers.py | 10 +++++----- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/examples/datasets/statsbomb.py b/examples/datasets/statsbomb.py index 9e1b1d8a..831f8b12 100644 --- a/examples/datasets/statsbomb.py +++ b/examples/datasets/statsbomb.py @@ -25,7 +25,7 @@ def main(): with performance_logging("transform", logger=logger): # convert to TRACAB coordinates dataset = dataset.transform( - to_orientation="FIXED_HOME_AWAY", + to_orientation="STATIC_HOME_AWAY", to_pitch_dimensions=[(-5500, 5500), (-3300, 3300)], ) diff --git a/kloppy/domain/models/common.py b/kloppy/domain/models/common.py index f6839c32..746d7412 100644 --- a/kloppy/domain/models/common.py +++ b/kloppy/domain/models/common.py @@ -273,13 +273,13 @@ class Orientation(Enum): The away team plays from left to right in the second period. AWAY_HOME: The away team plays from left to right in the first period. The home team plays from left to right in the second period. - FIXED_HOME_AWAY: The home team plays from left to right in both periods. - FIXED_AWAY_HOME: The away team plays from left to right in both periods. + STATIC_HOME_AWAY: The home team plays from left to right in both periods. + STATIC_AWAY_HOME: The away team plays from left to right in both periods. NOT_SET: The attacking direction is not defined. Notes: The attacking direction is not defined for penalty shootouts in the - `HOME_AWAY`, `AWAY_HOME`, `FIXED_HOME_AWAY`, and `FIXED_AWAY_HOME` + `HOME_AWAY`, `AWAY_HOME`, `STATIC_HOME_AWAY`, and `STATIC_AWAY_HOME` orientations. This period is ignored in orientation transforms involving one of these orientations and keeps its original attacking direction. @@ -296,8 +296,8 @@ class Orientation(Enum): AWAY_HOME = "away-home" # won't change during match - FIXED_HOME_AWAY = "fixed-home-away" - FIXED_AWAY_HOME = "fixed-away-home" + STATIC_HOME_AWAY = "fixed-home-away" + STATIC_AWAY_HOME = "fixed-away-home" # Not set in dataset NOT_SET = "not-set" @@ -342,9 +342,9 @@ def from_orientation( Returns: The attacking direction for the given data record. """ - if orientation == Orientation.FIXED_HOME_AWAY: + if orientation == Orientation.STATIC_HOME_AWAY: return AttackingDirection.LTR - if orientation == Orientation.FIXED_AWAY_HOME: + if orientation == Orientation.STATIC_AWAY_HOME: return AttackingDirection.RTL if orientation == Orientation.HOME_AWAY: if period is None: diff --git a/kloppy/tests/issues/issue_113/test_issue_113.py b/kloppy/tests/issues/issue_113/test_issue_113.py index ea60a709..b63d48ba 100644 --- a/kloppy/tests/issues/issue_113/test_issue_113.py +++ b/kloppy/tests/issues/issue_113/test_issue_113.py @@ -21,7 +21,7 @@ def kloppy_load_data(f7, f24): dataset = opta.load(f7_data=f7, f24_data=f24) events = dataset.transform( - to_orientation=Orientation.FIXED_HOME_AWAY + to_orientation=Orientation.STATIC_HOME_AWAY ).to_pandas( additional_columns={ "event_name": lambda event: str(getattr(event, "event_name", "")), diff --git a/kloppy/tests/test_helpers.py b/kloppy/tests/test_helpers.py index 1922aef0..a8bc5fc5 100644 --- a/kloppy/tests/test_helpers.py +++ b/kloppy/tests/test_helpers.py @@ -179,12 +179,12 @@ def test_transform_to_orientation(self): transform1.frames[1].attacking_direction == AttackingDirection.LTR ) - # Transform to FIXED_AWAY_HOME orientation + # Transform to STATIC_AWAY_HOME orientation transform2 = transform1.transform( - to_orientation=Orientation.FIXED_AWAY_HOME, + to_orientation=Orientation.STATIC_AWAY_HOME, to_pitch_dimensions=[[0, 1], [0, 1]], ) - assert transform2.metadata.orientation == Orientation.FIXED_AWAY_HOME + assert transform2.metadata.orientation == Orientation.STATIC_AWAY_HOME # all coordintes in the second half should be flipped assert transform2.frames[0].ball_coordinates == Point3D(x=0, y=1, z=0) assert transform2.frames[1].ball_coordinates == Point3D(x=0, y=1, z=1) @@ -296,7 +296,7 @@ def test_transform_event_data(self, base_dir): assert receipt_event.ball_owning_team == home_team transformed_dataset = dataset.transform( - to_orientation="fixed_home_away" + to_orientation="static_home_away" ) transformed_pressure_event = transformed_dataset.get_event_by_id( pressure_event.event_id @@ -336,7 +336,7 @@ def test_transform_event_data_freeze_frame(self, base_dir): "65f16e50-7c5d-4293-b2fc-d20887a772f9" ) transformed_dataset = dataset.transform( - to_orientation="fixed_away_home" + to_orientation="static_away_home" ) shot_event_transformed = transformed_dataset.get_event_by_id( shot_event.event_id From be7885e7cce6654951c2aadf62b67dfb811d489c Mon Sep 17 00:00:00 2001 From: Pieter Robberechts Date: Sun, 14 Jan 2024 18:02:54 +0100 Subject: [PATCH 6/6] linting --- kloppy/infra/serializers/event/sportec/deserializer.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/kloppy/infra/serializers/event/sportec/deserializer.py b/kloppy/infra/serializers/event/sportec/deserializer.py index 2507484a..5f43ff37 100644 --- a/kloppy/infra/serializers/event/sportec/deserializer.py +++ b/kloppy/infra/serializers/event/sportec/deserializer.py @@ -112,10 +112,7 @@ def sportec_metadata_from_xml_elm(match_root) -> SportecMetadata: if not away_team: raise DeserializationError("Away team is missing from metadata") - ( - home_score, - away_score, - ) = match_root.MatchInformation.General.attrib[ + (home_score, away_score,) = match_root.MatchInformation.General.attrib[ "Result" ].split(":") score = Score(home=int(home_score), away=int(away_score))