From 2c26866762eb84011e49475a7ad969d1b4d51d45 Mon Sep 17 00:00:00 2001 From: "UnravelSports [JB]" Date: Wed, 11 Jun 2025 13:38:35 +0200 Subject: [PATCH 1/4] indent issue? --- kloppy/domain/services/transformers/dataset.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kloppy/domain/services/transformers/dataset.py b/kloppy/domain/services/transformers/dataset.py index 3e2890669..8c07442aa 100644 --- a/kloppy/domain/services/transformers/dataset.py +++ b/kloppy/domain/services/transformers/dataset.py @@ -329,8 +329,8 @@ def transform_event(self, event: Event) -> Event: ): event = self.__flip_event(event) - if event.freeze_frame: - event.freeze_frame = self.transform_frame(event.freeze_frame) + if event.freeze_frame: + event.freeze_frame = self.transform_frame(event.freeze_frame) return event From 242f0c3155343565798baff07a9de540aa703ee3 Mon Sep 17 00:00:00 2001 From: "UnravelSports [JB]" Date: Wed, 18 Jun 2025 10:19:23 +0200 Subject: [PATCH 2/4] fix freeze frame double ball coordinate transform --- kloppy/domain/models/pitch.py | 1 + .../domain/services/transformers/dataset.py | 29 +++++++------- .../event/statsbomb/deserializer.py | 10 +++-- .../serializers/event/statsbomb/helpers.py | 7 +++- kloppy/tests/test_statsbomb.py | 38 +++++++++++++++++++ 5 files changed, 65 insertions(+), 20 deletions(-) diff --git a/kloppy/domain/models/pitch.py b/kloppy/domain/models/pitch.py index 6acec4845..2b4e7ce9a 100644 --- a/kloppy/domain/models/pitch.py +++ b/kloppy/domain/models/pitch.py @@ -417,6 +417,7 @@ def from_metric_base( Returns: The point in the regular pitch dimensions """ + if ( self.x_dim.min is None or self.x_dim.max is None diff --git a/kloppy/domain/services/transformers/dataset.py b/kloppy/domain/services/transformers/dataset.py index 3e2890669..d2d43a767 100644 --- a/kloppy/domain/services/transformers/dataset.py +++ b/kloppy/domain/services/transformers/dataset.py @@ -115,8 +115,6 @@ def change_point_dimensions( point_base = self._from_pitch_dimensions.to_metric_base( point, pitch_length=base_pitch_length, pitch_width=base_pitch_width ) - print(point_base) - print(self._to_pitch_dimensions.from_metric_base) point_to = self._to_pitch_dimensions.from_metric_base( point=point_base, pitch_length=base_pitch_length, @@ -179,26 +177,26 @@ def __needs_flip( ) return flip - def transform_frame(self, frame: Frame) -> Frame: + def transform_frame(self, frame: Frame, transform_ball_coordinates: bool = True) -> Frame: # Change coordinate system if self._needs_coordinate_system_change: - frame = self.__change_frame_coordinate_system(frame) + frame = self.__change_frame_coordinate_system(frame, transform_ball_coordinates=transform_ball_coordinates) # Change dimensions elif self._needs_pitch_dimensions_change: - frame = self.__change_frame_dimensions(frame) + frame = self.__change_frame_dimensions(frame, transform_ball_coordinates=transform_ball_coordinates) # Flip frame based on orientation if self._needs_orientation_change: if self.__needs_flip( period=frame.period, - ball_owning_team=frame.ball_owning_team, + ball_owning_team=frame.ball_owning_team ): - frame = self.__flip_frame(frame) + frame = self.__flip_frame(frame, transform_ball_coordinates=transform_ball_coordinates) return frame - def __change_frame_coordinate_system(self, frame: Frame): + def __change_frame_coordinate_system(self, frame: Frame, transform_ball_coordinates: bool = True): return Frame( # doesn't change timestamp=frame.timestamp, @@ -209,7 +207,7 @@ def __change_frame_coordinate_system(self, frame: Frame): # changes ball_coordinates=self.__change_point_coordinate_system( frame.ball_coordinates - ), + ) if transform_ball_coordinates else frame.ball_coordinates, ball_speed=frame.ball_speed, players_data={ key: PlayerData( @@ -226,7 +224,7 @@ def __change_frame_coordinate_system(self, frame: Frame): statistics=frame.statistics, ) - def __change_frame_dimensions(self, frame: Frame): + def __change_frame_dimensions(self, frame: Frame, transform_ball_coordinates: bool = True): return Frame( # doesn't change timestamp=frame.timestamp, @@ -237,7 +235,7 @@ def __change_frame_dimensions(self, frame: Frame): # changes ball_coordinates=self.change_point_dimensions( frame.ball_coordinates - ), + ) if transform_ball_coordinates else frame.ball_coordinates, players_data={ key: PlayerData( coordinates=self.change_point_dimensions( @@ -278,7 +276,6 @@ def __change_point_coordinate_system( point_base, y=base_pitch_width - point_base.y, ) - point_to = self._to_pitch_dimensions.from_metric_base( point_base, pitch_length=base_pitch_length, @@ -287,7 +284,7 @@ def __change_point_coordinate_system( return point_to - def __flip_frame(self, frame: Frame): + def __flip_frame(self, frame: Frame, transform_ball_coordinates: bool = True): players_data = {} for player, data in frame.players_data.items(): players_data[player] = PlayerData( @@ -305,7 +302,7 @@ def __flip_frame(self, frame: Frame): ball_state=frame.ball_state, period=frame.period, # changes - ball_coordinates=self.flip_point(frame.ball_coordinates), + ball_coordinates=self.flip_point(frame.ball_coordinates) if transform_ball_coordinates else frame.ball_coordinates, players_data=players_data, other_data=frame.other_data, statistics=frame.statistics, @@ -329,8 +326,8 @@ def transform_event(self, event: Event) -> Event: ): event = self.__flip_event(event) - if event.freeze_frame: - event.freeze_frame = self.transform_frame(event.freeze_frame) + if event.freeze_frame: + event.freeze_frame = self.transform_frame(event.freeze_frame) return event diff --git a/kloppy/infra/serializers/event/statsbomb/deserializer.py b/kloppy/infra/serializers/event/statsbomb/deserializer.py index ff007af07..2b7248238 100644 --- a/kloppy/infra/serializers/event/statsbomb/deserializer.py +++ b/kloppy/infra/serializers/event/statsbomb/deserializer.py @@ -69,6 +69,7 @@ def deserialize( # Create events with performance_logging("parse events", logger=logger): events = [] + freeze_frames = [] for raw_event in raw_events.values(): new_events = ( raw_event.set_version(data_version) @@ -80,7 +81,7 @@ def deserialize( # Transform event to the coordinate system event = self.transformer.transform_event(event) events.append(event) - + metadata = Metadata( teams=teams, periods=periods, @@ -94,6 +95,7 @@ def deserialize( **additional_metadata, ) dataset = EventDataset(metadata=metadata, records=events) + for event in dataset: if "freeze_frame" in event.raw_event.get("shot", {}): event.freeze_frame = self.transformer.transform_frame( @@ -103,7 +105,8 @@ def deserialize( away_team=teams[1], event=event, fidelity_version=data_version.shot_fidelity_version, - ) + ), + transform_ball_coordinates=False ) if not event.freeze_frame and event.event_id in three_sixty_data: freeze_frame = three_sixty_data[event.event_id] @@ -115,7 +118,8 @@ def deserialize( event=event, fidelity_version=data_version.xy_fidelity_version, visible_area=freeze_frame["visible_area"], - ) + ), + transform_ball_coordinates=False ) return dataset diff --git a/kloppy/infra/serializers/event/statsbomb/helpers.py b/kloppy/infra/serializers/event/statsbomb/helpers.py index 521ce2ec3..f96d4016e 100644 --- a/kloppy/infra/serializers/event/statsbomb/helpers.py +++ b/kloppy/infra/serializers/event/statsbomb/helpers.py @@ -109,6 +109,9 @@ def parse_freeze_frame( """Parse a freeze frame into a kloppy Frame.""" players_data = {} + if event.event_id == "d2097850-bd6b-4749-8db6-15ded7b6b118": + print("FF0", event.coordinates.x, event.coordinates.y) + def get_player_from_freeze_frame(player_data, team, i): if "player" in player_data: return team.get_player_by_id(player_data["player"]["id"]) @@ -149,7 +152,9 @@ def get_player_from_freeze_frame(player_data, team, i): event.period.start_timestamp.total_seconds() + event.timestamp.total_seconds() * FREEZE_FRAME_FPS ) - + if event.event_id == "d2097850-bd6b-4749-8db6-15ded7b6b118": + print("FF1", event.coordinates.x, event.coordinates.y) + frame = create_frame( frame_id=frame_id, ball_coordinates=Point3D( diff --git a/kloppy/tests/test_statsbomb.py b/kloppy/tests/test_statsbomb.py index c2d2e9a14..e6cc7c6a6 100644 --- a/kloppy/tests/test_statsbomb.py +++ b/kloppy/tests/test_statsbomb.py @@ -84,6 +84,24 @@ def dataset() -> EventDataset: assert dataset.dataset_type == DatasetType.EVENT return dataset +@pytest.fixture(scope="module") +def dataset_kl() -> EventDataset: + """Load StatsBomb data for Belgium - Portugal at Euro 2020""" + dataset = statsbomb.load( + event_data=f"{API_URL}/events/3794687.json", + lineup_data=f"{API_URL}/lineups/3794687.json", + three_sixty_data=f"{API_URL}/three-sixty/3794687.json", + coordinates="kloppy", + additional_metadata={ + "date": datetime(2020, 8, 23, 0, 0, tzinfo=timezone.utc), + "game_week": "7", + "game_id": "3888787", + "home_coach": "R. Martínez Montoliù", + "away_coach": "F. Fernandes da Costa Santos", + }, + ) + assert dataset.dataset_type == DatasetType.EVENT + return dataset def test_get_enum_type(): """Test retrieving enum types for StatsBomb IDs""" @@ -417,6 +435,21 @@ def get_color(player): base_dir / "outputs" / "test_statsbomb_freeze_frame_shot.png" ) + def test_freeze_frame_360_transform(self, dataset_kl: EventDataset): + post_transform_pass = dataset_kl.transform( + to_coordinate_system="secondspectrum", + to_orientation="ACTION_EXECUTING_TEAM" + ).filter(lambda event: event.event_type == EventType.PASS)[4] + print("ID", post_transform_pass.event_id) + print("X", dataset_kl.get_event_by_id( + post_transform_pass.event_id + ).coordinates) + print("A", post_transform_pass.coordinates) + print("B", post_transform_pass.freeze_frame.ball_coordinates) + assert post_transform_pass.coordinates.x == post_transform_pass.freeze_frame.ball_coordinates.x + assert post_transform_pass.coordinates.y == post_transform_pass.freeze_frame.ball_coordinates.y + + def test_freeze_frame_360(self, dataset: EventDataset, base_dir: Path): """Test if 360 freeze-frame is properly parsed and attached to shot events""" pass_event = dataset.get_event_by_id( @@ -436,6 +469,11 @@ def test_freeze_frame_360(self, dataset: EventDataset, base_dir: Path): pass_event.player ] assert event_player_coordinates == pass_event.coordinates + + dataset.transform( + to_coordinate_system="secondspectrum", + to_orientation="ACTION_EXECUTING_TEAM" + ) # The freeze-frame should contain the location of all players coordinates_per_team = defaultdict(list) From 7000d7ed671af6e8b5593603d83ab4b632634add Mon Sep 17 00:00:00 2001 From: "UnravelSports [JB]" Date: Wed, 18 Jun 2025 10:23:08 +0200 Subject: [PATCH 3/4] removed prints --- kloppy/infra/serializers/event/statsbomb/helpers.py | 5 ----- kloppy/tests/test_statsbomb.py | 10 +--------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/kloppy/infra/serializers/event/statsbomb/helpers.py b/kloppy/infra/serializers/event/statsbomb/helpers.py index 9b7e3d551..521ce2ec3 100644 --- a/kloppy/infra/serializers/event/statsbomb/helpers.py +++ b/kloppy/infra/serializers/event/statsbomb/helpers.py @@ -109,9 +109,6 @@ def parse_freeze_frame( """Parse a freeze frame into a kloppy Frame.""" players_data = {} - if event.event_id == "d2097850-bd6b-4749-8db6-15ded7b6b118": - print("FF0", event.coordinates.x, event.coordinates.y) - def get_player_from_freeze_frame(player_data, team, i): if "player" in player_data: return team.get_player_by_id(player_data["player"]["id"]) @@ -152,8 +149,6 @@ def get_player_from_freeze_frame(player_data, team, i): event.period.start_timestamp.total_seconds() + event.timestamp.total_seconds() * FREEZE_FRAME_FPS ) - if event.event_id == "d2097850-bd6b-4749-8db6-15ded7b6b118": - print("FF1", event.coordinates.x, event.coordinates.y) frame = create_frame( frame_id=frame_id, diff --git a/kloppy/tests/test_statsbomb.py b/kloppy/tests/test_statsbomb.py index a878558a4..74d5bd5b9 100644 --- a/kloppy/tests/test_statsbomb.py +++ b/kloppy/tests/test_statsbomb.py @@ -442,15 +442,7 @@ def test_freeze_frame_360_transform(self, dataset_kl: EventDataset): to_coordinate_system="secondspectrum", to_orientation="ACTION_EXECUTING_TEAM", ).filter(lambda event: event.event_type == EventType.PASS)[4] - print("ID", post_transform_pass.event_id) - print( - "X", - dataset_kl.get_event_by_id( - post_transform_pass.event_id - ).coordinates, - ) - print("A", post_transform_pass.coordinates) - print("B", post_transform_pass.freeze_frame.ball_coordinates) + assert ( post_transform_pass.coordinates.x == post_transform_pass.freeze_frame.ball_coordinates.x From afe302d93b56794e6b7d97aba828588f6fd593d3 Mon Sep 17 00:00:00 2001 From: "UnravelSports [JB]" Date: Wed, 18 Jun 2025 10:31:07 +0200 Subject: [PATCH 4/4] clean up dangling thing --- kloppy/tests/test_statsbomb.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/kloppy/tests/test_statsbomb.py b/kloppy/tests/test_statsbomb.py index 74d5bd5b9..3f7ebda7a 100644 --- a/kloppy/tests/test_statsbomb.py +++ b/kloppy/tests/test_statsbomb.py @@ -472,11 +472,6 @@ def test_freeze_frame_360(self, dataset: EventDataset, base_dir: Path): ] assert event_player_coordinates == pass_event.coordinates - dataset.transform( - to_coordinate_system="secondspectrum", - to_orientation="ACTION_EXECUTING_TEAM", - ) - # The freeze-frame should contain the location of all players coordinates_per_team = defaultdict(list) for (