From 00da3ff29ed7da3f91bda31072baca646d467b1e Mon Sep 17 00:00:00 2001 From: MKlaasman Date: Wed, 5 Jul 2023 16:46:29 +0200 Subject: [PATCH] =?UTF-8?q?Notes:=201.=09Remained=20with=20Pieter=E2=80=99?= =?UTF-8?q?s=20initial=20proposal=20of=20only=20adding:=20AERIAL,=20GROUND?= =?UTF-8?q?,=20LOOSE=5FBALL=20&=20SLIDING=5FTACKLE=202.=09StatsBomb:=20Che?= =?UTF-8?q?cked=20qualifiers=20with=20=E2=80=9Cname=E2=80=9D=20instead=20o?= =?UTF-8?q?f=20id,=20since=20ids=20are=20not=20consistent=20in=20StatsBomb?= =?UTF-8?q?=20open=20data.=20As=20per=20StatsBomb=20helpdesk.=203.=09Added?= =?UTF-8?q?=20a=20method:=20.get=5Fqualifier=5Fvalues()=20.=20Which=20retu?= =?UTF-8?q?rns=20a=20list=20of=20Qualifiers=20instead=20of=20.get=5Fqualif?= =?UTF-8?q?ier=5Fvalue(),=20that=20returns=20the=20first=20Qualifier.=204.?= =?UTF-8?q?=09Also=20Added=20NEUTRAL=20as=20outcome,=20since=20this=20is?= =?UTF-8?q?=20provided=20in=20wyscout=5Fv2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kloppy/_providers/datafactory.py | 1 - kloppy/_providers/opta.py | 1 - kloppy/_providers/statsbomb.py | 1 - kloppy/domain/models/common.py | 4 - kloppy/domain/models/event.py | 5 +- .../domain/services/state_builder/__init__.py | 1 - .../state_builder/builders/sequence.py | 1 - .../domain/services/transformers/attribute.py | 1 - .../services/transformers/data_record.py | 1 - .../domain/services/transformers/dataset.py | 13 --- .../event/datafactory/deserializer.py | 1 - .../event/metrica/json_deserializer.py | 5 - .../serializers/event/opta/deserializer.py | 39 ++++--- .../event/statsbomb/deserializer.py | 100 ++++++++---------- .../event/wyscout/deserializer_v2.py | 50 +++++++-- .../event/wyscout/deserializer_v3.py | 88 +++++++++------ .../event/wyscout/wyscout_events.py | 1 + .../infra/serializers/tracking/metrica_csv.py | 1 - .../tracking/metrica_epts/deserializer.py | 1 - .../serializers/tracking/secondspectrum.py | 4 - kloppy/infra/serializers/tracking/tracab.py | 1 - kloppy/tests/files/wyscout_events_v3.json | 3 +- kloppy/tests/test_opta.py | 11 +- kloppy/tests/test_state_builder.py | 8 +- kloppy/tests/test_statsbomb.py | 11 +- kloppy/tests/test_to_records.py | 2 +- kloppy/tests/test_wyscout.py | 34 +++++- kloppy/utils.py | 3 - 28 files changed, 222 insertions(+), 170 deletions(-) diff --git a/kloppy/_providers/datafactory.py b/kloppy/_providers/datafactory.py index 4ab71932..4b642e4e 100644 --- a/kloppy/_providers/datafactory.py +++ b/kloppy/_providers/datafactory.py @@ -28,7 +28,6 @@ def load( event_factory=event_factory or get_config("event_factory"), ) with open_as_file(event_data) as event_data_fp: - return deserializer.deserialize( inputs=DatafactoryInputs(event_data=event_data_fp), ) diff --git a/kloppy/_providers/opta.py b/kloppy/_providers/opta.py index 729057c1..c62f0395 100644 --- a/kloppy/_providers/opta.py +++ b/kloppy/_providers/opta.py @@ -32,7 +32,6 @@ def load( with open_as_file(f7_data) as f7_data_fp, open_as_file( f24_data ) as f24_data_fp: - return deserializer.deserialize( inputs=OptaInputs(f7_data=f7_data_fp, f24_data=f24_data_fp), ) diff --git a/kloppy/_providers/statsbomb.py b/kloppy/_providers/statsbomb.py index 1fd26e35..1d750bc0 100644 --- a/kloppy/_providers/statsbomb.py +++ b/kloppy/_providers/statsbomb.py @@ -48,7 +48,6 @@ def load( ) as lineup_data_fp, open_as_file( Source.create(three_sixty_data, optional=True) ) as three_sixty_data_fp: - return deserializer.deserialize( inputs=StatsBombInputs( event_data=event_data_fp, diff --git a/kloppy/domain/models/common.py b/kloppy/domain/models/common.py index b6b86018..4b60b893 100644 --- a/kloppy/domain/models/common.py +++ b/kloppy/domain/models/common.py @@ -426,7 +426,6 @@ def vertical_orientation(self) -> VerticalOrientation: @property def pitch_dimensions(self) -> PitchDimensions: - if self.length is not None and self.width is not None: return PitchDimensions( x_dim=Dimension(0, 1), @@ -656,7 +655,6 @@ def pitch_dimensions(self) -> PitchDimensions: def build_coordinate_system(provider: Provider, **kwargs): - if provider == Provider.TRACAB: return TracabCoordinateSystem(normalized=False, **kwargs) @@ -966,7 +964,6 @@ def to_records( as_list: bool = True, **named_columns: "Column", ) -> Union[List[Dict[str, Any]], Iterable[Dict[str, Any]]]: - from ..services.transformers.data_record import get_transformer_cls transformer = get_transformer_cls(self.dataset_type)( @@ -984,7 +981,6 @@ def to_dict( orient: Literal["list"] = "list", **named_columns: "Column", ) -> Dict[str, List[Any]]: - if orient == "list": from ..services.transformers.data_record import get_transformer_cls diff --git a/kloppy/domain/models/event.py b/kloppy/domain/models/event.py index 35d02630..befc5e28 100644 --- a/kloppy/domain/models/event.py +++ b/kloppy/domain/models/event.py @@ -140,10 +140,12 @@ class DuelResult(ResultType): Attributes: WON (DuelResult): When winning the duel (player touching the ball first) LOST (DuelResult): When losing the duel (opponent touches the ball first) + NEUTRAL (DuelResult): When neither player wins duel [Mainly for WyScout v2] """ WON = "WON" LOST = "LOST" + NEUTRAL = "NEUTRAL" @property def is_success(self): @@ -385,14 +387,12 @@ class DuelType(Enum): GROUND (DuelType): A duel when the ball is on the ground. LOOSE_BALL (DuelType): When the ball is not under the control of any particular player or team. SLIDING_TACKLE (DuelType): A duel where the player slides on the ground to kick the ball away from an opponent. - STANDING_TACKLE (DuelType): A duel where the player makes a standing tackle. """ AERIAL = "AERIAL" GROUND = "GROUND" LOOSE_BALL = "LOOSE_BALL" SLIDING_TACKLE = "SLIDING_TACKLE" - STANDING_TACKLE = "STANDING_TACKLE" @dataclass @@ -469,7 +469,6 @@ def get_qualifier_value(self, qualifier_type: Type[Qualifier]): return qualifier.value return None - def get_qualifier_values(self, qualifier_type: Type[Qualifier]): """ Returns all Qualifiers of a certain type, or None if qualifier is not present. diff --git a/kloppy/domain/services/state_builder/__init__.py b/kloppy/domain/services/state_builder/__init__.py index 7577cb16..3fcca8bb 100644 --- a/kloppy/domain/services/state_builder/__init__.py +++ b/kloppy/domain/services/state_builder/__init__.py @@ -36,7 +36,6 @@ def add_state(dataset: EventDataset, *builder_keys: List[str]) -> EventDataset: events = [] for event in dataset.events: - state = { builder_key: builder.reduce_before(state[builder_key], event) for builder_key, builder in builders.items() diff --git a/kloppy/domain/services/state_builder/builders/sequence.py b/kloppy/domain/services/state_builder/builders/sequence.py index 8bebde82..e851f430 100644 --- a/kloppy/domain/services/state_builder/builders/sequence.py +++ b/kloppy/domain/services/state_builder/builders/sequence.py @@ -43,7 +43,6 @@ def reduce_before(self, state: Sequence, event: Event) -> Sequence: return state def reduce_after(self, state: Sequence, event: Event) -> Sequence: - if isinstance(event, CLOSE_SEQUENCE): state = replace( state, sequence_id=state.sequence_id + 1, team=None diff --git a/kloppy/domain/services/transformers/attribute.py b/kloppy/domain/services/transformers/attribute.py index 0ab53d19..09bdcf02 100644 --- a/kloppy/domain/services/transformers/attribute.py +++ b/kloppy/domain/services/transformers/attribute.py @@ -276,7 +276,6 @@ def __call__(self, frame: Frame) -> Dict[str, Any]: else None, ) for player, player_data in frame.players_data.items(): - row.update( { f"{player.player_id}_x": player_data.coordinates.x diff --git a/kloppy/domain/services/transformers/data_record.py b/kloppy/domain/services/transformers/data_record.py index 93ace117..8eb8f503 100644 --- a/kloppy/domain/services/transformers/data_record.py +++ b/kloppy/domain/services/transformers/data_record.py @@ -25,7 +25,6 @@ def __init__( **named_columns: Union[str, Callable[[T], Any]], ): if not columns and not named_columns: - converter = self.default_transformer() else: default = self.default_transformer() diff --git a/kloppy/domain/services/transformers/dataset.py b/kloppy/domain/services/transformers/dataset.py index b2001895..22ee0a70 100644 --- a/kloppy/domain/services/transformers/dataset.py +++ b/kloppy/domain/services/transformers/dataset.py @@ -30,7 +30,6 @@ def __init__( to_pitch_dimensions: Optional[PitchDimensions] = None, to_orientation: Optional[Orientation] = None, ): - if ( from_pitch_dimensions and from_coordinate_system @@ -90,7 +89,6 @@ def _needs_pitch_dimensions_change(self): def change_point_dimensions( self, point: Union[Point, Point3D, None] ) -> Union[Point, Point3D, None]: - if point is None: return None @@ -108,7 +106,6 @@ def change_point_dimensions( def flip_point( self, point: Union[Point, Point3D, None] ) -> Union[Point, Point3D, None]: - if not point: return None @@ -160,7 +157,6 @@ def __needs_flip( return flip def transform_frame(self, frame: Frame) -> Frame: - # Change coordinate system if self._needs_coordinate_system_change: frame = self.__change_frame_coordinate_system(frame) @@ -178,7 +174,6 @@ def transform_frame(self, frame: Frame) -> Frame: return frame def __change_frame_coordinate_system(self, frame: Frame): - return Frame( # doesn't change timestamp=frame.timestamp, @@ -205,7 +200,6 @@ def __change_frame_coordinate_system(self, frame: Frame): ) def __change_frame_dimensions(self, frame: Frame): - return Frame( # doesn't change timestamp=frame.timestamp, @@ -234,7 +228,6 @@ def __change_frame_dimensions(self, frame: Frame): def __change_point_coordinate_system( self, point: Union[Point, Point3D, None] ) -> Union[Point, Point3D, None]: - if not point: return None @@ -257,7 +250,6 @@ def __change_point_coordinate_system( return Point(x=x, y=y) def __flip_frame(self, frame: Frame): - players_data = {} for player, data in frame.players_data.items(): players_data[player] = PlayerData( @@ -281,7 +273,6 @@ def __flip_frame(self, frame: Frame): ) def transform_event(self, event: Event) -> Event: - # Change coordinate system if self._needs_coordinate_system_change: event = self.__change_event_coordinate_system(event) @@ -303,7 +294,6 @@ def transform_event(self, event: Event) -> Event: return event def __change_event_coordinate_system(self, event: Event): - position_changes = { field.name: self.__change_point_coordinate_system( getattr(event, field.name) @@ -316,7 +306,6 @@ def __change_event_coordinate_system(self, event: Event): return replace(event, **position_changes) def __change_event_dimensions(self, event: Event): - position_changes = { field.name: self.change_point_dimensions( getattr(event, field.name) @@ -329,7 +318,6 @@ def __change_event_dimensions(self, event: Event): return replace(event, **position_changes) def __flip_event(self, event: Event): - position_changes = { field.name: self.flip_point(getattr(event, field.name)) for field in fields(event) @@ -350,7 +338,6 @@ def transform_dataset( to_orientation: Optional[Orientation] = None, to_coordinate_system: Optional[CoordinateSystem] = None, ) -> Dataset: - if ( to_pitch_dimensions is None and to_orientation is None diff --git a/kloppy/infra/serializers/event/datafactory/deserializer.py b/kloppy/infra/serializers/event/datafactory/deserializer.py index c32a3653..2bac39c0 100644 --- a/kloppy/infra/serializers/event/datafactory/deserializer.py +++ b/kloppy/infra/serializers/event/datafactory/deserializer.py @@ -353,7 +353,6 @@ def provider(self) -> Provider: return Provider.DATAFACTORY def deserialize(self, inputs: DatafactoryInputs) -> EventDataset: - transformer = self.get_transformer(length=2, width=2) with performance_logging("load data", logger=logger): diff --git a/kloppy/infra/serializers/event/metrica/json_deserializer.py b/kloppy/infra/serializers/event/metrica/json_deserializer.py index 0320d475..8100afc1 100644 --- a/kloppy/infra/serializers/event/metrica/json_deserializer.py +++ b/kloppy/infra/serializers/event/metrica/json_deserializer.py @@ -118,7 +118,6 @@ def _parse_subtypes(event: dict) -> List: def _parse_pass( event: Dict, previous_event: Dict, subtypes: List, team: Team ) -> Dict: - event_type_id = event["type"]["id"] if event_type_id == MS_PASS_OUTCOME_COMPLETE: @@ -157,7 +156,6 @@ def _parse_pass( def _get_event_qualifiers( event: Dict, previous_event: Dict, subtypes: List ) -> List[Qualifier]: - qualifiers = [] qualifiers.extend(_get_event_setpiece_qualifiers(previous_event, subtypes)) @@ -169,7 +167,6 @@ def _get_event_qualifiers( def _get_event_setpiece_qualifiers( previous_event: Dict, subtypes: List ) -> List[Qualifier]: - qualifiers = [] previous_event_type_id = previous_event["type"]["id"] if previous_event_type_id == MS_SET_PIECE: @@ -193,7 +190,6 @@ def _get_event_setpiece_qualifiers( def _get_event_bodypart_qualifiers(subtypes: List) -> List[Qualifier]: - qualifiers = [] if subtypes and MS_BODY_PART_HEAD in subtypes: qualifiers.append(BodyPartQualifier(value=BodyPart.HEAD)) @@ -274,7 +270,6 @@ def deserialize(self, inputs: MetricaJsonEventDataInputs) -> EventDataset: with performance_logging("parse data", logger=logger): events = [] for i, raw_event in enumerate(raw_events["data"]): - if raw_event["team"]["id"] == metadata.teams[0].team_id: team = metadata.teams[0] elif raw_event["team"]["id"] == metadata.teams[1].team_id: diff --git a/kloppy/infra/serializers/event/opta/deserializer.py b/kloppy/infra/serializers/event/opta/deserializer.py index 7e9011ce..e19f59b3 100644 --- a/kloppy/infra/serializers/event/opta/deserializer.py +++ b/kloppy/infra/serializers/event/opta/deserializer.py @@ -20,6 +20,8 @@ ShotResult, TakeOnResult, DuelResult, + DuelType, + DuelQualifier, Ground, Score, Provider, @@ -41,8 +43,6 @@ BodyPart, PassType, PassQualifier, - DuelType, - DuelQualifier ) from kloppy.exceptions import DeserializationError from kloppy.infra.serializers.event.deserializer import EventDataDeserializer @@ -319,8 +319,22 @@ def _parse_shot( def _parse_duel(raw_qualifiers: List, type_id: int, outcome: int) -> Dict: qualifiers = _get_event_qualifiers(raw_qualifiers) - duel_qualifiers = _get_duel_qualifiers(type_id) - qualifiers.extend(duel_qualifiers) + if type_id == EVENT_TYPE_TACKLE: + qualifiers.extend([DuelQualifier(value=DuelType.GROUND)]) + elif type_id == EVENT_TYPE_AERIAL: + qualifiers.extend( + [ + DuelQualifier(value=DuelType.LOOSE_BALL), + DuelQualifier(value=DuelType.AERIAL), + ] + ) + elif type_id == EVENT_TYPE_50_50: + qualifiers.extend( + [ + DuelQualifier(value=DuelType.LOOSE_BALL), + DuelQualifier(value=DuelType.GROUND), + ] + ) if outcome: result = DuelResult.WON @@ -475,17 +489,6 @@ def _get_event_card_qualifiers(raw_qualifiers: List) -> List[Qualifier]: return qualifiers -def _get_duel_qualifiers(type_id: int) -> List[Qualifier]: - if type_id == EVENT_TYPE_TACKLE: - duel_qualifiers = [DuelQualifier(value=DuelType.GROUND), DuelQualifier(value=DuelType.STANDING_TACKLE)] - elif type_id == EVENT_TYPE_AERIAL: - duel_qualifiers = [DuelQualifier(value=DuelType.LOOSE_BALL), DuelQualifier(value=DuelType.AERIAL)] - elif type_id == EVENT_TYPE_50_50: - duel_qualifiers = [DuelQualifier(value=DuelType.LOOSE_BALL), DuelQualifier(value=DuelType.GROUND)] - - return duel_qualifiers - - def _get_event_type_name(type_id: int) -> str: return event_type_names.get(type_id, "unknown") @@ -676,12 +679,16 @@ def deserialize(self, inputs: OptaInputs) -> EventDataset: qualifiers=None, **generic_event_kwargs, ) + elif type_id in DUEL_EVENTS: - duel_event_kwargs = _parse_duel(raw_qualifiers, type_id, outcome) + duel_event_kwargs = _parse_duel( + raw_qualifiers, type_id, outcome + ) event = self.event_factory.build_duel( **duel_event_kwargs, **generic_event_kwargs, ) + elif type_id == EVENT_TYPE_FOUL_COMMITTED: event = self.event_factory.build_foul_committed( result=None, diff --git a/kloppy/infra/serializers/event/statsbomb/deserializer.py b/kloppy/infra/serializers/event/statsbomb/deserializer.py index 97f82a7b..e7708043 100644 --- a/kloppy/infra/serializers/event/statsbomb/deserializer.py +++ b/kloppy/infra/serializers/event/statsbomb/deserializer.py @@ -87,24 +87,13 @@ SB_EVENT_TYPE_AERIAL_LOST = 10 SB_EVENT_TYPE_TACKLE = 11 -# SB_50_50_OUTCOME_WON = 108 -# SB_50_50_OUTCOME_LOST = 109 -# SB_50_50_OUTCOME_SUCCESS_TO_OPPOSITION = 2 # TODO: Documentation says: 148 - Asking StatsBomb support -# SB_50_50_OUTCOME_SUCCESS_TO_TEAM = 3 # TODO: Documentation says: 147 - Asking StatsBomb support -# -# SB_DUEL_OUTCOME_LOST = 1 -# SB_DUEL_OUTCOME_WON = 4 -# SB_DUEL_OUTCOME_LOST_IN_PLAY = 13 -# SB_DUEL_OUTCOME_LOST_OUT = 14 -# SB_DUEL_OUTCOME_SUCCESS = 15 -# SB_DUEL_OUTCOME_SUCCESS_IN_PLAY = 16 -# SB_DUEL_OUTCOME_SUCCESS_OUT = 17 -DUEL_WON_NAMES = ["Won", "Success To Team", "Success", "Success In Play", "Success Out"] -DUEL_LOST_NAMES = ["Lost", "Aerial Lost", "Success To Opposition", "Lost In Play", "Lost Out"] -# DUEL_WON_IDS = [SB_50_50_OUTCOME_WON, SB_50_50_OUTCOME_SUCCESS_TO_TEAM, SB_DUEL_OUTCOME_WON, SB_DUEL_OUTCOME_SUCCESS, -# SB_DUEL_OUTCOME_SUCCESS_IN_PLAY, SB_DUEL_OUTCOME_SUCCESS_OUT] -# DUEL_LOST_IDS = [SB_50_50_OUTCOME_LOST, SB_50_50_OUTCOME_SUCCESS_TO_OPPOSITION, SB_DUEL_OUTCOME_LOST, -# SB_DUEL_OUTCOME_LOST_IN_PLAY, SB_DUEL_OUTCOME_LOST_OUT, SB_EVENT_TYPE_AERIAL_LOST] +DUEL_WON_NAMES = [ + "Won", + "Success To Team", + "Success", + "Success In Play", + "Success Out", +] SB_EVENT_TYPE_FREE_KICK = 62 SB_EVENT_TYPE_THROW_IN = 67 @@ -443,56 +432,55 @@ def _parse_take_on(take_on_dict: Dict) -> Dict: } -def _parse_duel(raw_event: dict, event_type: int, ) -> Dict: - qualifiers = [] +def _parse_duel( + raw_event: dict, + event_type: int, +) -> Dict: + duel_dict = None + duel_qualifiers = [] if event_type == SB_EVENT_TYPE_DUEL: - duel_dict = raw_event["duel"] - if "type" in duel_dict: - type_id = duel_dict["type"]["id"] - if type_id == SB_EVENT_TYPE_AERIAL_LOST: - duel_qualifiers = [DuelQualifier(value=DuelType.LOOSE_BALL), DuelQualifier(value=DuelType.AERIAL)] - elif type_id == SB_EVENT_TYPE_TACKLE: - duel_qualifiers = [DuelQualifier(value=DuelType.GROUND), DuelQualifier(value=DuelType.STANDING_TACKLE)] + duel_dict = raw_event.get("duel", {}) + type_id = duel_dict.get("type", {}).get("id") + if type_id == SB_EVENT_TYPE_AERIAL_LOST: + duel_qualifiers = [ + DuelQualifier(value=DuelType.LOOSE_BALL), + DuelQualifier(value=DuelType.AERIAL), + ] + elif type_id == SB_EVENT_TYPE_TACKLE: + duel_qualifiers = [DuelQualifier(value=DuelType.GROUND)] elif event_type == SB_EVENT_TYPE_50_50: - duel_dict = raw_event["50_50"] - duel_qualifiers = [DuelQualifier(value=DuelType.LOOSE_BALL), DuelQualifier(value=DuelType.GROUND)] + duel_dict = raw_event.get("50_50", {}) + duel_qualifiers = [ + DuelQualifier(value=DuelType.LOOSE_BALL), + DuelQualifier(value=DuelType.GROUND), + ] - qualifiers.extend(duel_qualifiers) - body_part_qualifiers = _get_body_part_qualifiers(duel_dict) - qualifiers.extend(body_part_qualifiers) + qualifiers = duel_qualifiers + _get_body_part_qualifiers(duel_dict) - if "outcome" in duel_dict: - outcome_name = duel_dict["outcome"]["name"] - else: - outcome_name = duel_dict["type"]["name"] + outcome_name = duel_dict.get("outcome", {}).get("name") or duel_dict.get( + "type", {} + ).get("name") - result = None if outcome_name in DUEL_WON_NAMES: result = DuelResult.WON - elif outcome_name in DUEL_LOST_NAMES: + else: result = DuelResult.LOST - return { - "result": result, - "qualifiers": qualifiers - } + return {"result": result, "qualifiers": qualifiers} def _parse_aerial_won_duel(raw_event: dict, type_name: str) -> Dict: - qualifiers = [] aerial_won_dict = raw_event[type_name] - duel_qualifiers = [DuelQualifier(value=DuelType.LOOSE_BALL), DuelQualifier(value=DuelType.AERIAL)] - result = DuelResult.WON + duel_qualifiers = [ + DuelQualifier(value=DuelType.LOOSE_BALL), + DuelQualifier(value=DuelType.AERIAL), + ] + qualifiers = duel_qualifiers + _get_body_part_qualifiers(aerial_won_dict) - qualifiers.extend(duel_qualifiers) - body_part_qualifiers = _get_body_part_qualifiers(aerial_won_dict) - qualifiers.extend(body_part_qualifiers) + result = DuelResult.WON - return { - "result": result, - "qualifiers": qualifiers - } + return {"result": result, "qualifiers": qualifiers} def _parse_substitution(substitution_dict: Dict, team: Team) -> Dict: @@ -908,9 +896,12 @@ def deserialize(self, inputs: StatsBombInputs) -> EventDataset: ) new_events.append(generic_event) - # Add possible aerial won - Last, since applicable to multiple event types + # Add possible aerial won - Applicable to multiple event types for type_name in ["shot", "clearance", "miscontrol", "pass"]: - if type_name in raw_event and "aerial_won" in raw_event[type_name]: + if ( + type_name in raw_event + and "aerial_won" in raw_event[type_name] + ): duel_event_kwargs = _parse_aerial_won_duel( raw_event=raw_event, type_name=type_name ) @@ -920,7 +911,6 @@ def deserialize(self, inputs: StatsBombInputs) -> EventDataset: ) new_events.append(duel_event) - for event in new_events: if self.should_include_event(event): transformed_event = transformer.transform_event(event) diff --git a/kloppy/infra/serializers/event/wyscout/deserializer_v2.py b/kloppy/infra/serializers/event/wyscout/deserializer_v2.py index 3fae4729..d989ebf4 100644 --- a/kloppy/infra/serializers/event/wyscout/deserializer_v2.py +++ b/kloppy/infra/serializers/event/wyscout/deserializer_v2.py @@ -10,6 +10,9 @@ CardType, CounterAttackQualifier, Dimension, + DuelResult, + DuelQualifier, + DuelType, EventDataset, FoulCommittedEvent, GenericEvent, @@ -33,8 +36,6 @@ SetPieceType, ShotEvent, ShotResult, - TakeOnEvent, - TakeOnResult, Team, ) from kloppy.utils import performance_logging @@ -245,13 +246,44 @@ def _parse_set_piece(raw_event: Dict, next_event: Dict) -> Dict: return result -def _parse_takeon(raw_event: Dict) -> Dict: +def _parse_duel(raw_event: Dict) -> Dict: qualifiers = _generic_qualifiers(raw_event) + duel_qualifiers = [] + + sub_event_id = raw_event["subEventId"] + + if sub_event_id == wyscout_events.DUEL.AERIAL: + duel_qualifiers.extend( + [ + DuelQualifier(value=DuelType.LOOSE_BALL), + DuelQualifier(value=DuelType.AERIAL), + ] + ) + elif sub_event_id in [ + wyscout_events.DUEL.GROUND_ATTACKING, + wyscout_events.DUEL.GROUND_DEFENDING, + ]: + duel_qualifiers.extend([DuelQualifier(value=DuelType.GROUND)]) + elif sub_event_id == wyscout_events.DUEL.GROUND_LOOSE_BALL: + duel_qualifiers.extend( + [ + DuelQualifier(value=DuelType.LOOSE_BALL), + DuelQualifier(value=DuelType.GROUND), + ] + ) + + if _has_tag(raw_event, wyscout_tags.SLIDING_TACKLE): + duel_qualifiers.extend([DuelQualifier(value=DuelType.SLIDING_TACKLE)]) + + qualifiers.extend(duel_qualifiers) + result = None - if _has_tag(raw_event, wyscout_tags.LOST): - result = TakeOnResult.INCOMPLETE if _has_tag(raw_event, wyscout_tags.WON): - result = TakeOnResult.COMPLETE + result = DuelResult.WON + elif _has_tag(raw_event, wyscout_tags.LOST): + result = DuelResult.LOST + elif _has_tag(raw_event, wyscout_tags.NEUTRAL): + result = DuelResult.NEUTRAL return {"result": result, "qualifiers": qualifiers} @@ -386,9 +418,9 @@ def deserialize(self, inputs: WyscoutInputs) -> EventDataset: **recovery_event_args, **generic_event_args ) elif raw_event["eventId"] == wyscout_events.DUEL.EVENT: - takeon_event_args = _parse_takeon(raw_event) - event = self.event_factory.build_take_on( - **takeon_event_args, **generic_event_args + duel_event_args = _parse_duel(raw_event) + event = self.event_factory.build_duel( + **duel_event_args, **generic_event_args ) elif raw_event["eventId"] not in [ wyscout_events.SAVE.EVENT, diff --git a/kloppy/infra/serializers/event/wyscout/deserializer_v3.py b/kloppy/infra/serializers/event/wyscout/deserializer_v3.py index 7804e7c7..de599b5d 100644 --- a/kloppy/infra/serializers/event/wyscout/deserializer_v3.py +++ b/kloppy/infra/serializers/event/wyscout/deserializer_v3.py @@ -253,44 +253,64 @@ def _parse_set_piece(raw_event: Dict, next_event: Dict, team: Team) -> Dict: def _parse_duel(raw_event: Dict) -> Dict: qualifiers = _generic_qualifiers(raw_event) - - result = None duel_qualifiers = [] - if "loose_ball_duel" in raw_event["type"]["secondary"]: - duel_qualifiers.extend([DuelQualifier(value=DuelType.LOOSE_BALL)]) - if "ground_duel" in raw_event["type"]["secondary"]: - duel_qualifiers.extend([DuelQualifier(value=DuelType.GROUND)]) - if "sliding_tackle" in raw_event["type"]["secondary"]: - duel_qualifiers.extend([DuelQualifier(value=DuelType.GROUND), DuelQualifier(value=DuelType.SLIDING_TACKLE)]) - if "aerial_duel" in raw_event["type"]["secondary"]: - duel_qualifiers.extend([DuelQualifier(value=DuelType.LOOSE_BALL), DuelQualifier(value=DuelType.AERIAL)]) - + secondary_types = raw_event["type"]["secondary"] + + if "ground_duel" in secondary_types: + duel_qualifiers.append(DuelQualifier(value=DuelType.GROUND)) + elif "aerial_duel" in secondary_types: + duel_qualifiers.extend( + [ + DuelQualifier(value=DuelType.LOOSE_BALL), + DuelQualifier(value=DuelType.AERIAL), + ] + ) + else: + if ( + "loose_ball_duel" in secondary_types + and "sliding_tackle" in secondary_types + ): + duel_qualifiers.extend( + [ + DuelQualifier(value=DuelType.GROUND), + DuelQualifier(value=DuelType.LOOSE_BALL), + DuelQualifier(value=DuelType.SLIDING_TACKLE), + ] + ) + elif "loose_ball_duel" in secondary_types: + duel_qualifiers.extend( + [ + DuelQualifier(value=DuelType.GROUND), + DuelQualifier(value=DuelType.LOOSE_BALL), + ] + ) + elif "sliding_tackle" in secondary_types: + duel_qualifiers.extend( + [ + DuelQualifier(value=DuelType.GROUND), + DuelQualifier(value=DuelType.SLIDING_TACKLE), + ] + ) qualifiers.extend(duel_qualifiers) - # get result value - if "offensive_duel" in raw_event["type"]["secondary"]: - if raw_event["groundDuel"]["keptPossession"]: - result = DuelResult.WON - else: - result = DuelResult.LOST - elif "defensive_duel" in raw_event["type"]["secondary"]: - if raw_event["groundDuel"]["recoveredPossession"]: - result = DuelResult.WON - else: - result = DuelResult.LOST - elif "aerial_duel" in raw_event["type"]["secondary"]: - duel_qualifiers.extend([DuelQualifier(value=DuelType.AERIAL)]) - if raw_event["aerialDuel"]["firstTouch"]: - result = DuelResult.WON - else: - result = DuelResult.LOST - # elif "sliding_tackle" in raw_event["type"]["secondary"]: - # duel_qualifiers.extend([DuelQualifier(value=DuelType.AERIAL)]) - # if raw_event["aerialDuel"]["firstTouch"]: - # result = DuelResult.WON - # else: - # result = DuelResult.LOST + if ( + "offensive_duel" in secondary_types + and raw_event["groundDuel"]["keptPossession"] + ): + result = DuelResult.WON + elif ( + "defensive_duel" in secondary_types + and raw_event["groundDuel"]["recoveredPossession"] + ): + result = DuelResult.WON + elif ( + "aerial_duel" in secondary_types + and raw_event["aerialDuel"]["firstTouch"] + ): + result = DuelResult.WON + else: + result = DuelResult.LOST return {"result": result, "qualifiers": qualifiers} diff --git a/kloppy/infra/serializers/event/wyscout/wyscout_events.py b/kloppy/infra/serializers/event/wyscout/wyscout_events.py index 13370070..02154718 100644 --- a/kloppy/infra/serializers/event/wyscout/wyscout_events.py +++ b/kloppy/infra/serializers/event/wyscout/wyscout_events.py @@ -4,6 +4,7 @@ class DUEL: EVENT = 1 + AERIAL = 10 GROUND_ATTACKING = 11 GROUND_DEFENDING = 12 GROUND_LOOSE_BALL = 13 diff --git a/kloppy/infra/serializers/tracking/metrica_csv.py b/kloppy/infra/serializers/tracking/metrica_csv.py index 47f08158..fa61690c 100644 --- a/kloppy/infra/serializers/tracking/metrica_csv.py +++ b/kloppy/infra/serializers/tracking/metrica_csv.py @@ -123,7 +123,6 @@ def __create_iterator( def __validate_partials( home_partial_frame: __PartialFrame, away_partial_frame: __PartialFrame ): - if home_partial_frame.frame_id != away_partial_frame.frame_id: raise ValueError( f"frame_id mismatch: home {home_partial_frame.frame_id}, " diff --git a/kloppy/infra/serializers/tracking/metrica_epts/deserializer.py b/kloppy/infra/serializers/tracking/metrica_epts/deserializer.py index 7e9dac7a..efb0d5b3 100644 --- a/kloppy/infra/serializers/tracking/metrica_epts/deserializer.py +++ b/kloppy/infra/serializers/tracking/metrica_epts/deserializer.py @@ -51,7 +51,6 @@ def _frame_from_row( players_data = {} for team in metadata.teams: for player in team.players: - other_data = {} for sensor in other_sensors: player_sensor_field_str = f"player_{player.player_id}_{sensor.channels[0].channel_id}" diff --git a/kloppy/infra/serializers/tracking/secondspectrum.py b/kloppy/infra/serializers/tracking/secondspectrum.py index 57277c23..cd3f4e0c 100644 --- a/kloppy/infra/serializers/tracking/secondspectrum.py +++ b/kloppy/infra/serializers/tracking/secondspectrum.py @@ -55,7 +55,6 @@ def provider(self) -> Provider: @classmethod def _frame_from_framedata(cls, teams, period, frame_data): - frame_id = frame_data["frameIdx"] frame_timestamp = frame_data["gameClock"] @@ -75,7 +74,6 @@ def _frame_from_framedata(cls, teams, period, frame_data): players_data = {} for team, team_str in zip(teams, ["homePlayers", "awayPlayers"]): for player_data in frame_data[team_str]: - jersey_no = player_data["number"] x, y, _ = player_data["xyz"] player = team.get_player_by_jersey_number(jersey_no) @@ -111,7 +109,6 @@ def __validate_inputs(inputs: Dict[str, Readable]): raise ValueError("Please specify a value for 'raw_data'") def deserialize(self, inputs: SecondSpectrumInputs) -> TrackingDataset: - metadata = None # Handles the XML metadata that contains the pitch dimensions and frame info @@ -201,7 +198,6 @@ def deserialize(self, inputs: SecondSpectrumInputs) -> TrackingDataset: teams, ["homePlayers", "awayPlayers"] ): for player_data in metadata[team_str]: - # We use the attributes field of Player to store the extra IDs provided by the # metadata. We designate the player_id to be the 'optaId' field as this is what's # used as 'player_id' in the raw frame data file diff --git a/kloppy/infra/serializers/tracking/tracab.py b/kloppy/infra/serializers/tracking/tracab.py index 03962e3b..f7a0e162 100644 --- a/kloppy/infra/serializers/tracking/tracab.py +++ b/kloppy/infra/serializers/tracking/tracab.py @@ -156,7 +156,6 @@ def deserialize(self, inputs: TRACABInputs) -> TrackingDataset: ) with performance_logging("Loading data", logger=logger): - transformer = self.get_transformer( length=pitch_size_width, width=pitch_size_height ) diff --git a/kloppy/tests/files/wyscout_events_v3.json b/kloppy/tests/files/wyscout_events_v3.json index d7eeba1d..689a0abf 100644 --- a/kloppy/tests/files/wyscout_events_v3.json +++ b/kloppy/tests/files/wyscout_events_v3.json @@ -441,7 +441,8 @@ "primary": "duel", "secondary": [ "ground_duel", - "offensive_duel" + "offensive_duel", + "loose_ball_duel" ] }, "location": { diff --git a/kloppy/tests/test_opta.py b/kloppy/tests/test_opta.py index 179355a9..1be4888c 100644 --- a/kloppy/tests/test_opta.py +++ b/kloppy/tests/test_opta.py @@ -110,9 +110,14 @@ def test_correct_deserialization(self, f7_data: str, f24_data: str): assert dataset.events[20].receiver_coordinates.x == 89.3 # 2360555167 # Check DuelQualifiers - assert dataset.events[7].get_qualifier_values(DuelQualifier)[1].value == DuelType.AERIAL - assert dataset.events[8].get_qualifier_values(DuelQualifier)[1].value == DuelType.GROUND - assert dataset.events[16].get_qualifier_values(DuelQualifier)[1].value == DuelType.STANDING_TACKLE + assert ( + dataset.events[7].get_qualifier_values(DuelQualifier)[1].value + == DuelType.AERIAL + ) + assert ( + dataset.events[8].get_qualifier_values(DuelQualifier)[1].value + == DuelType.GROUND + ) def test_correct_normalized_deserialization( self, f7_data: str, f24_data: str diff --git a/kloppy/tests/test_state_builder.py b/kloppy/tests/test_state_builder.py index 343c73e7..592812ac 100644 --- a/kloppy/tests/test_state_builder.py +++ b/kloppy/tests/test_state_builder.py @@ -29,10 +29,10 @@ def test_score_state_builder(self, base_dir): events_per_score[str(score)] = len(events) assert events_per_score == { - "0-0": 2898, + "0-0": 2909, "1-0": 717, "2-0": 405, - "3-0": 3, + "3-0": 8, } def test_sequence_state_builder(self, base_dir): @@ -92,8 +92,8 @@ def test_formation_state_builder(self, base_dir): events_per_formation_change[str(formation)] = len(events) # inspect FormationChangeEvent usage and formation state_builder - assert events_per_formation_change["4-1-4-1"] == 3074 - assert events_per_formation_change["4-4-2"] == 949 + assert events_per_formation_change["4-1-4-1"] == 3085 + assert events_per_formation_change["4-4-2"] == 954 assert dataset.metadata.teams[0].starting_formation == FormationType( "4-4-2" diff --git a/kloppy/tests/test_statsbomb.py b/kloppy/tests/test_statsbomb.py index 82020ba6..4a6d329c 100644 --- a/kloppy/tests/test_statsbomb.py +++ b/kloppy/tests/test_statsbomb.py @@ -150,9 +150,14 @@ def test_correct_deserialization( assert dataset.events[3400].get_qualifier_value(PassQualifier) is None - assert dataset.events[194].get_qualifier_values(DuelQualifier)[1].value == DuelType.AERIAL - assert dataset.events[307].get_qualifier_values(DuelQualifier)[1].value == DuelType.STANDING_TACKLE - assert dataset.events[4032].get_qualifier_values(DuelQualifier)[1].value == DuelType.GROUND + assert ( + dataset.events[194].get_qualifier_values(DuelQualifier)[1].value + == DuelType.AERIAL + ) + assert ( + dataset.events[4032].get_qualifier_values(DuelQualifier)[1].value + == DuelType.GROUND + ) def test_correct_normalized_deserialization( self, lineup_data: Path, event_data: Path diff --git a/kloppy/tests/test_to_records.py b/kloppy/tests/test_to_records.py index 72201016..3fc1f4e8 100644 --- a/kloppy/tests/test_to_records.py +++ b/kloppy/tests/test_to_records.py @@ -29,7 +29,7 @@ def dataset(self, event_data: Path, lineup_data: Path) -> EventDataset: def test_default_columns(self, dataset: EventDataset): records = dataset.to_records() - assert len(records) == 4023 + assert len(records) == 4039 assert list(records[0].keys()) == [ "event_id", "event_type", diff --git a/kloppy/tests/test_wyscout.py b/kloppy/tests/test_wyscout.py index fab413aa..dde07963 100644 --- a/kloppy/tests/test_wyscout.py +++ b/kloppy/tests/test_wyscout.py @@ -1,7 +1,13 @@ from pathlib import Path import pytest -from kloppy.domain import Point, SetPieceType, SetPieceQualifier +from kloppy.domain import ( + Point, + SetPieceType, + SetPieceQualifier, + DuelQualifier, + DuelType, +) from kloppy import wyscout @@ -25,6 +31,19 @@ def test_correct_v3_deserialization(self, event_v3_data: Path): ) assert dataset.records[2].coordinates == Point(36.0, 78.0) + assert ( + dataset.events[5].get_qualifier_value(DuelQualifier) + == DuelType.GROUND + ) + assert ( + dataset.events[6].get_qualifier_values(DuelQualifier)[1].value + == DuelType.AERIAL + ) + assert ( + dataset.events[7].get_qualifier_values(DuelQualifier)[2].value + == DuelType.SLIDING_TACKLE + ) + def test_correct_normalized_v3_deserialization(self, event_v3_data: Path): dataset = wyscout.load(event_data=event_v3_data, data_version="V3") assert dataset.records[2].coordinates == Point(0.36, 0.78) @@ -37,6 +56,19 @@ def test_correct_v2_deserialization(self, event_v2_data: Path): ) assert dataset.records[2].coordinates == Point(29.0, 6.0) + assert ( + dataset.events[39].get_qualifier_value(DuelQualifier) + == DuelType.GROUND + ) + assert ( + dataset.events[43].get_qualifier_values(DuelQualifier)[1].value + == DuelType.AERIAL + ) + assert ( + dataset.events[258].get_qualifier_values(DuelQualifier)[2].value + == DuelType.SLIDING_TACKLE + ) + def test_correct_auto_recognize_deserialization(self, event_v2_data: Path): dataset = wyscout.load(event_data=event_v2_data, coordinates="wyscout") assert dataset.records[2].coordinates == Point(29.0, 6.0) diff --git a/kloppy/utils.py b/kloppy/utils.py index fe71fae4..e235ff0c 100644 --- a/kloppy/utils.py +++ b/kloppy/utils.py @@ -96,7 +96,6 @@ def deprecated(reason): """ if isinstance(reason, string_types): - # The @deprecated is used with a 'reason'. # # .. code-block:: python @@ -106,7 +105,6 @@ def deprecated(reason): # pass def decorator(func1): - if inspect.isclass(func1): fmt1 = "Call to deprecated class {name} ({reason})." else: @@ -128,7 +126,6 @@ def new_func1(*args, **kwargs): return decorator elif inspect.isclass(reason) or inspect.isfunction(reason): - # The @deprecated is used without any 'reason'. # # .. code-block:: python