From c5dca588e96e487c77564e72e26be83072b845f6 Mon Sep 17 00:00:00 2001 From: driesdeprest Date: Mon, 4 Dec 2023 13:59:38 +0100 Subject: [PATCH 1/4] Identify artificial formation change event for Wyscout --- .../event/wyscout/deserializer_v3.py | 65 ++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/kloppy/infra/serializers/event/wyscout/deserializer_v3.py b/kloppy/infra/serializers/event/wyscout/deserializer_v3.py index 33fbfdca..15ab6fd0 100644 --- a/kloppy/infra/serializers/event/wyscout/deserializer_v3.py +++ b/kloppy/infra/serializers/event/wyscout/deserializer_v3.py @@ -41,6 +41,7 @@ TakeOnResult, Team, ) +from kloppy.exceptions import DeserializationError from kloppy.utils import performance_logging from ..deserializer import EventDataDeserializer @@ -363,6 +364,41 @@ def _parse_duel(raw_event: Dict) -> Dict: return {"result": result, "qualifiers": qualifiers} +def get_home_away_team_formation(event, team, home_team, away_team): + # TODO: map formation from provider to kloppy + if team.team_id == home_team.team_id: + current_home_team_formation = event["team"]["formation"] + current_away_team_formation = event["opponentTeam"]["formation"] + elif team.team_id == away_team.team_id: + current_away_team_formation = event["team"]["formation"] + current_home_team_formation = event["opponentTeam"]["formation"] + else: + raise DeserializationError(f"Unknown team_id {team.team_id}") + + return current_home_team_formation, current_away_team_formation + + +def identify_artificial_formation_change_event( + event, next_event, team, home_team, away_team +): + event_formation_change_info = {} + ( + current_home_team_formation, + current_away_team_formation, + ) = get_home_away_team_formation(event, team, home_team, away_team) + ( + next_home_team_formation, + next_away_team_formation, + ) = get_home_away_team_formation(next_event, team, home_team, away_team) + if next_home_team_formation != current_home_team_formation: + event_formation_change_info[home_team] = next_home_team_formation + + if next_away_team_formation != current_away_team_formation: + event_formation_change_info[away_team] = next_away_team_formation + + return event_formation_change_info + + def _players_to_dict(players: List[Player]): return {player.player_id: player for player in players} @@ -547,12 +583,39 @@ def deserialize(self, inputs: WyscoutInputs) -> EventDataset: result=None, qualifiers=_generic_qualifiers(raw_event), event_name=raw_event["type"]["primary"], - **generic_event_args + **generic_event_args, ) if event and self.should_include_event(event): events.append(transformer.transform_event(event)) + event_formation_change_info = ( + identify_artificial_formation_change_event( + event, next_event, team, home_team, away_team + ) + ) + for ( + formation_change_team, + formation_change_event_kwargs, + ) in event_formation_change_info.items(): + generic_event_args.update( + { + "id": None, + "raw_event": None, + "coordinates": None, + "player": None, + "team": formation_change_team, + } + ) + event = self.event_factory.build_formation_change( + result=None, + qualifiers=None, + **formation_change_event_kwargs, + **generic_event_args, + ) + if event and self.should_include_event(event): + events.append(transformer.transform_event(event)) + metadata = Metadata( teams=[home_team, away_team], periods=periods, From b2e0b6386044d4a23abbfd21432501143bb049f9 Mon Sep 17 00:00:00 2001 From: driesdeprest Date: Mon, 4 Dec 2023 15:51:39 +0100 Subject: [PATCH 2/4] Fix logic and include mapping --- .../event/wyscout/deserializer_v3.py | 110 ++++++++++++------ kloppy/tests/files/wyscout_events_v3.json | 6 +- kloppy/tests/test_wyscout.py | 4 + 3 files changed, 79 insertions(+), 41 deletions(-) diff --git a/kloppy/infra/serializers/event/wyscout/deserializer_v3.py b/kloppy/infra/serializers/event/wyscout/deserializer_v3.py index 15ab6fd0..567b551a 100644 --- a/kloppy/infra/serializers/event/wyscout/deserializer_v3.py +++ b/kloppy/infra/serializers/event/wyscout/deserializer_v3.py @@ -40,6 +40,7 @@ TakeOnEvent, TakeOnResult, Team, + FormationType, ) from kloppy.exceptions import DeserializationError from kloppy.utils import performance_logging @@ -53,6 +54,29 @@ INVALID_PLAYER = "0" +formations = { + "4-4-2": FormationType.FOUR_FOUR_TWO, + "4-4-1-1": FormationType.FOUR_FOUR_ONE_ONE, + "4-3-2-1": FormationType.FOUR_THREE_TWO_ONE, + "4-2-3-1": FormationType.FOUR_TWO_THREE_ONE, + "4-1-4-1": FormationType.FOUR_ONE_FOUR_ONE, + "4-1-3-2": FormationType.FOUR_ONE_THREE_TWO, + "4-3-1-2": FormationType.FOUR_THREE_ONE_TWO, + "4-3-3": FormationType.FOUR_THREE_THREE, + "4-5-1": FormationType.FOUR_FIVE_ONE, + "4-2-2-2": FormationType.FOUR_TWO_TWO_TWO, + "4-2-1-3": FormationType.FOUR_TWO_ONE_THREE, + "3-4-3": FormationType.THREE_FOUR_THREE, + "3-4-1-2": FormationType.THREE_FOUR_ONE_TWO, + "3-4-2-1": FormationType.THREE_FOUR_TWO_ONE, + "3-5-2": FormationType.THREE_FIVE_TWO, + "3-5-1-1": FormationType.THREE_FIVE_ONE_ONE, + "5-3-2": FormationType.FIVE_THREE_TWO, + "5-4-1": FormationType.FIVE_FOUR_ONE, + "3-3-3-1": FormationType.THREE_THREE_THREE_ONE, + "3-2-3-2": FormationType.THREE_TWO_THREE_TWO, +} + def _parse_team(raw_events, wyId: str, ground: Ground) -> Team: team = Team( @@ -364,14 +388,17 @@ def _parse_duel(raw_event: Dict) -> Dict: return {"result": result, "qualifiers": qualifiers} -def get_home_away_team_formation(event, team, home_team, away_team): - # TODO: map formation from provider to kloppy - if team.team_id == home_team.team_id: - current_home_team_formation = event["team"]["formation"] - current_away_team_formation = event["opponentTeam"]["formation"] - elif team.team_id == away_team.team_id: - current_away_team_formation = event["team"]["formation"] - current_home_team_formation = event["opponentTeam"]["formation"] +def get_home_away_team_formation(event, team): + if team.ground == Ground.HOME: + current_home_team_formation = formations[event["team"]["formation"]] + current_away_team_formation = formations[ + event["opponentTeam"]["formation"] + ] + elif team.ground == Ground.AWAY: + current_away_team_formation = formations[event["team"]["formation"]] + current_home_team_formation = formations[ + event["opponentTeam"]["formation"] + ] else: raise DeserializationError(f"Unknown team_id {team.team_id}") @@ -379,22 +406,28 @@ def get_home_away_team_formation(event, team, home_team, away_team): def identify_artificial_formation_change_event( - event, next_event, team, home_team, away_team + raw_event, raw_next_event, teams, home_team, away_team ): + current_event_team = teams[str(raw_event["team"]["id"])] + next_event_team = teams[str(raw_next_event["team"]["id"])] event_formation_change_info = {} ( current_home_team_formation, current_away_team_formation, - ) = get_home_away_team_formation(event, team, home_team, away_team) + ) = get_home_away_team_formation(raw_event, current_event_team) ( next_home_team_formation, next_away_team_formation, - ) = get_home_away_team_formation(next_event, team, home_team, away_team) + ) = get_home_away_team_formation(raw_next_event, next_event_team) if next_home_team_formation != current_home_team_formation: - event_formation_change_info[home_team] = next_home_team_formation + event_formation_change_info[home_team] = { + "formation_type": next_home_team_formation + } if next_away_team_formation != current_away_team_formation: - event_formation_change_info[away_team] = next_away_team_formation + event_formation_change_info[away_team] = { + "formation_type": next_away_team_formation + } return event_formation_change_info @@ -589,32 +622,33 @@ def deserialize(self, inputs: WyscoutInputs) -> EventDataset: if event and self.should_include_event(event): events.append(transformer.transform_event(event)) - event_formation_change_info = ( - identify_artificial_formation_change_event( - event, next_event, team, home_team, away_team - ) - ) - for ( - formation_change_team, - formation_change_event_kwargs, - ) in event_formation_change_info.items(): - generic_event_args.update( - { - "id": None, - "raw_event": None, - "coordinates": None, - "player": None, - "team": formation_change_team, - } - ) - event = self.event_factory.build_formation_change( - result=None, - qualifiers=None, - **formation_change_event_kwargs, - **generic_event_args, + if next_event: + event_formation_change_info = ( + identify_artificial_formation_change_event( + raw_event, next_event, teams, home_team, away_team + ) ) - if event and self.should_include_event(event): - events.append(transformer.transform_event(event)) + for ( + formation_change_team, + formation_change_event_kwargs, + ) in event_formation_change_info.items(): + generic_event_args.update( + { + "event_id": None, + "raw_event": None, + "coordinates": None, + "player": None, + "team": formation_change_team, + } + ) + event = self.event_factory.build_formation_change( + result=None, + qualifiers=None, + **formation_change_event_kwargs, + **generic_event_args, + ) + if event and self.should_include_event(event): + events.append(transformer.transform_event(event)) metadata = Metadata( teams=[home_team, away_team], diff --git a/kloppy/tests/files/wyscout_events_v3.json b/kloppy/tests/files/wyscout_events_v3.json index e09dc036..8fce7109 100644 --- a/kloppy/tests/files/wyscout_events_v3.json +++ b/kloppy/tests/files/wyscout_events_v3.json @@ -946,12 +946,12 @@ "y": 90 }, "team": { - "formation": "4-2-3-1", + "formation": "4-3-3", "id": 3166, "name": "Bologna" }, "opponentTeam": { - "formation": "3-4-3", + "formation": "4-4-2", "id": 3185, "name": "Torino" }, @@ -983,7 +983,7 @@ "y": 10 }, "team": { - "formation": "4-2-3-1", + "formation": "4-3-3", "id": 3166, "name": "Bologna" }, diff --git a/kloppy/tests/test_wyscout.py b/kloppy/tests/test_wyscout.py index 3449c0f5..2fcf8303 100644 --- a/kloppy/tests/test_wyscout.py +++ b/kloppy/tests/test_wyscout.py @@ -56,6 +56,10 @@ def test_correct_v3_deserialization(self, event_v3_data: Path): ) assert dataset.events[9].event_type == EventType.CLEARANCE assert dataset.events[12].event_type == EventType.INTERCEPTION + assert ( + dataset.events[13].event_type == EventType.FORMATION_CHANGE + and dataset.events[14].event_type == EventType.FORMATION_CHANGE + ) def test_correct_normalized_v3_deserialization(self, event_v3_data: Path): dataset = wyscout.load(event_data=event_v3_data, data_version="V3") From 3f8a9f4e22f2e963843bc455999ed4a43dfae287 Mon Sep 17 00:00:00 2001 From: driesdeprest Date: Fri, 8 Dec 2023 11:42:08 +0100 Subject: [PATCH 3/4] Rename to synthetic and add prefix to event ID --- kloppy/infra/serializers/event/wyscout/deserializer_v3.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/kloppy/infra/serializers/event/wyscout/deserializer_v3.py b/kloppy/infra/serializers/event/wyscout/deserializer_v3.py index 567b551a..3f4be492 100644 --- a/kloppy/infra/serializers/event/wyscout/deserializer_v3.py +++ b/kloppy/infra/serializers/event/wyscout/deserializer_v3.py @@ -405,7 +405,7 @@ def get_home_away_team_formation(event, team): return current_home_team_formation, current_away_team_formation -def identify_artificial_formation_change_event( +def identify_synthetic_formation_change_event( raw_event, raw_next_event, teams, home_team, away_team ): current_event_team = teams[str(raw_event["team"]["id"])] @@ -624,7 +624,7 @@ def deserialize(self, inputs: WyscoutInputs) -> EventDataset: if next_event: event_formation_change_info = ( - identify_artificial_formation_change_event( + identify_synthetic_formation_change_event( raw_event, next_event, teams, home_team, away_team ) ) @@ -634,7 +634,7 @@ def deserialize(self, inputs: WyscoutInputs) -> EventDataset: ) in event_formation_change_info.items(): generic_event_args.update( { - "event_id": None, + "event_id": f"synthetic-{raw_event['id']}", "raw_event": None, "coordinates": None, "player": None, From 6bfdb3f89689a596f49f7593886f6ac5a088ed73 Mon Sep 17 00:00:00 2001 From: driesdeprest Date: Thu, 14 Dec 2023 09:21:29 +0100 Subject: [PATCH 4/4] Fix failing test as result of merge conflict --- kloppy/tests/test_wyscout.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kloppy/tests/test_wyscout.py b/kloppy/tests/test_wyscout.py index f407a932..1852aa41 100644 --- a/kloppy/tests/test_wyscout.py +++ b/kloppy/tests/test_wyscout.py @@ -56,11 +56,11 @@ def test_correct_v3_deserialization(self, event_v3_data: Path): ) assert dataset.events[9].event_type == EventType.CLEARANCE assert dataset.events[12].event_type == EventType.INTERCEPTION - assert dataset.events[14].event_type == EventType.TAKE_ON assert ( dataset.events[13].event_type == EventType.FORMATION_CHANGE and dataset.events[14].event_type == EventType.FORMATION_CHANGE ) + assert dataset.events[18].event_type == EventType.TAKE_ON def test_correct_normalized_v3_deserialization(self, event_v3_data: Path): dataset = wyscout.load(event_data=event_v3_data, data_version="V3")