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 67a256a3..f1f48a5d 100644 --- a/kloppy/domain/models/event.py +++ b/kloppy/domain/models/event.py @@ -133,6 +133,28 @@ def is_success(self): return self == self.COMPLETE +class DuelResult(ResultType): + """ + DuelResult + + 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): + """ + Returns if the duel was won + """ + return self == self.WON + + class CardType(Enum): """ CardType @@ -157,6 +179,7 @@ class EventType(Enum): TAKE_ON (EventType): CARRY (EventType): CLEARANCE (EventType): + DUEL (EventType): SUBSTITUTION (EventType): CARD (EventType): PLAYER_ON (EventType): @@ -174,6 +197,7 @@ class EventType(Enum): TAKE_ON = "TAKE_ON" CARRY = "CARRY" CLEARANCE = "CLEARANCE" + DUEL = "DUEL" SUBSTITUTION = "SUBSTITUTION" CARD = "CARD" PLAYER_ON = "PLAYER_ON" @@ -356,6 +380,28 @@ class GoalkeeperActionQualifier(EnumQualifier): value: GoalkeeperAction +class DuelType(Enum): + """ + DuelType + + Attributes: + AERIAL (DuelType): A duel when the ball is in the air and loose. + 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. + """ + + AERIAL = "AERIAL" + GROUND = "GROUND" + LOOSE_BALL = "LOOSE_BALL" + SLIDING_TACKLE = "SLIDING_TACKLE" + + +@dataclass +class DuelQualifier(EnumQualifier): + value: DuelType + + @dataclass class CounterAttackQualifier(BoolQualifier): pass @@ -425,6 +471,27 @@ 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. + + Arguments: + qualifier_type: one of the following QualifierTypes: [`SetPieceQualifier`][kloppy.domain.models.event.SetPieceQualifier] + [`BodyPartQualifier`][kloppy.domain.models.event.BodyPartQualifier] [`PassQualifier`][kloppy.domain.models.event.PassQualifier] + + Examples: + >>> from kloppy.domain import SetPieceQualifier + >>> pass_event.get_qualifier_value(SetPieceQualifier) + + """ + qualifiers = [] + if self.qualifiers: + for qualifier in self.qualifiers: + if isinstance(qualifier, qualifier_type): + qualifiers.append(qualifier) + + return qualifiers + def get_related_events(self) -> List["Event"]: if not self.dataset: raise OrphanedRecordError() @@ -667,6 +734,22 @@ class ClearanceEvent(Event): event_name: str = "clearance" +@dataclass(repr=False) +@docstring_inherit_attributes(Event) +class DuelEvent(Event): + """ + DuelEvent + + Attributes: + event_type (EventType): `EventType.DUEL` (See [`EventType`][kloppy.domain.models.event.EventType]) + event_name (str): `"duel"` + + """ + + event_type: EventType = EventType.DUEL + event_name: str = "duel" + + @dataclass(repr=False) @docstring_inherit_attributes(Event) class SubstitutionEvent(Event): @@ -905,4 +988,8 @@ def generic_record_converter(event: Event): "GoalkeeperAction", "GoalkeeperActionQualifier", "CounterAttackQualifier", + "DuelEvent", + "DuelType", + "DuelQualifier", + "DuelResult", ] diff --git a/kloppy/domain/services/event_factory.py b/kloppy/domain/services/event_factory.py index 5a8cfec6..0f550ba5 100644 --- a/kloppy/domain/services/event_factory.py +++ b/kloppy/domain/services/event_factory.py @@ -10,6 +10,7 @@ TakeOnEvent, RecoveryEvent, CarryEvent, + DuelEvent, ClearanceEvent, FormationChangeEvent, BallOutEvent, @@ -86,6 +87,9 @@ def build_carry(self, **kwargs) -> CarryEvent: def build_clearance(self, **kwargs) -> ClearanceEvent: return create_event(ClearanceEvent, **kwargs) + def build_duel(self, **kwargs) -> DuelEvent: + return create_event(DuelEvent, **kwargs) + def build_formation_change(self, **kwargs) -> FormationChangeEvent: return create_event(FormationChangeEvent, **kwargs) 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 52899b1d..c8407655 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, @@ -206,7 +201,6 @@ def __change_frame_coordinate_system(self, frame: Frame): ) def __change_frame_dimensions(self, frame: Frame): - return Frame( # doesn't change timestamp=frame.timestamp, @@ -235,7 +229,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 @@ -258,7 +251,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( @@ -282,7 +274,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) @@ -304,7 +295,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) @@ -317,7 +307,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) @@ -330,7 +319,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) @@ -351,7 +339,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 b78937f1..38e235ec 100644 --- a/kloppy/infra/serializers/event/opta/deserializer.py +++ b/kloppy/infra/serializers/event/opta/deserializer.py @@ -12,16 +12,28 @@ BallState, DatasetFlag, Orientation, + PassEvent, + ShotEvent, + TakeOnEvent, + GenericEvent, PassResult, ShotResult, TakeOnResult, + DuelResult, + DuelType, + DuelQualifier, Ground, Score, Provider, Metadata, Player, Position, + RecoveryEvent, + BallOutEvent, + FoulCommittedEvent, + FormationChangeEvent, FormationType, + CardEvent, CardType, CardQualifier, Qualifier, @@ -46,6 +58,9 @@ EVENT_TYPE_PASS = 1 EVENT_TYPE_OFFSIDE_PASS = 2 EVENT_TYPE_TAKE_ON = 3 +EVENT_TYPE_TACKLE = 7 +EVENT_TYPE_AERIAL = 44 +EVENT_TYPE_50_50 = 67 EVENT_TYPE_CLEARANCE = 12 EVENT_TYPE_SHOT_MISS = 13 EVENT_TYPE_SHOT_POST = 14 @@ -59,6 +74,7 @@ EVENT_TYPE_FORMATION_CHANGE = 40 BALL_OUT_EVENTS = [EVENT_TYPE_BALL_OUT, EVENT_TYPE_CORNER_AWARDED] +DUEL_EVENTS = [EVENT_TYPE_TACKLE, EVENT_TYPE_AERIAL, EVENT_TYPE_50_50] BALL_OWNING_EVENTS = ( EVENT_TYPE_PASS, @@ -314,6 +330,33 @@ def _parse_shot( return dict(coordinates=coordinates, result=result, qualifiers=qualifiers) +def _parse_duel(raw_qualifiers: List, type_id: int, outcome: int) -> Dict: + qualifiers = _get_event_qualifiers(raw_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), + ] + ) + + result = DuelResult.WON if outcome else DuelResult.LOST + + return dict( + result=result, + qualifiers=qualifiers, + ) + + def _parse_team_players( f7_root, team_ref: str ) -> Tuple[str, Dict[str, Dict[str, str]]]: @@ -622,7 +665,6 @@ def deserialize(self, inputs: OptaInputs) -> EventDataset: elif type_id == EVENT_TYPE_TAKE_ON: take_on_event_kwargs = _parse_take_on(outcome) event = self.event_factory.build_take_on( - qualifiers=None, **take_on_event_kwargs, **generic_event_kwargs, ) @@ -666,6 +708,14 @@ def deserialize(self, inputs: OptaInputs) -> EventDataset: **clearance_event_kwargs, **generic_event_kwargs, ) + elif type_id in DUEL_EVENTS: + 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) and ( outcome == 0 ): diff --git a/kloppy/infra/serializers/event/statsbomb/deserializer.py b/kloppy/infra/serializers/event/statsbomb/deserializer.py index 71f41c67..017b583c 100644 --- a/kloppy/infra/serializers/event/statsbomb/deserializer.py +++ b/kloppy/infra/serializers/event/statsbomb/deserializer.py @@ -15,6 +15,9 @@ ShotResult, TakeOnResult, CarryResult, + DuelResult, + DuelQualifier, + DuelType, Metadata, Ground, Player, @@ -43,10 +46,12 @@ logger = logging.getLogger(__name__) SB_EVENT_TYPE_RECOVERY = 2 +SB_EVENT_TYPE_DUEL = 4 SB_EVENT_TYPE_CLEARANCE = 9 SB_EVENT_TYPE_DRIBBLE = 14 SB_EVENT_TYPE_SHOT = 16 SB_EVENT_TYPE_PASS = 30 +SB_EVENT_TYPE_50_50 = 33 SB_EVENT_TYPE_CARRY = 43 SB_EVENT_TYPE_HALF_START = 18 @@ -80,6 +85,17 @@ SB_SHOT_OUTCOME_SAVED_OFF_TARGET = 115 SB_SHOT_OUTCOME_SAVED_TO_POST = 116 +SB_EVENT_TYPE_AERIAL_LOST = 10 +SB_EVENT_TYPE_TACKLE = 11 + +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 SB_EVENT_TYPE_KICK_OFF = 65 @@ -436,6 +452,56 @@ def _parse_take_on(take_on_dict: Dict) -> Dict: } +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.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.get("50_50", {}) + duel_qualifiers = [ + DuelQualifier(value=DuelType.LOOSE_BALL), + DuelQualifier(value=DuelType.GROUND), + ] + + qualifiers = duel_qualifiers + _get_body_part_qualifiers(duel_dict) + + outcome_name = duel_dict.get("outcome", {}).get("name") or duel_dict.get( + "type", {} + ).get("name") + + result = ( + DuelResult.WON if outcome_name in DUEL_WON_NAMES else DuelResult.LOST + ) + + return {"result": result, "qualifiers": qualifiers} + + +def _parse_aerial_won_duel(raw_event: dict, type_name: str) -> Dict: + aerial_won_dict = raw_event[type_name] + duel_qualifiers = [ + DuelQualifier(value=DuelType.LOOSE_BALL), + DuelQualifier(value=DuelType.AERIAL), + ] + qualifiers = duel_qualifiers + _get_body_part_qualifiers(aerial_won_dict) + + result = DuelResult.WON + + return {"result": result, "qualifiers": qualifiers} + + def _parse_substitution(substitution_dict: Dict, team: Team) -> Dict: replacement_player = None for player in team.players: @@ -753,6 +819,15 @@ def deserialize(self, inputs: StatsBombInputs) -> EventDataset: **generic_event_kwargs, ) new_events.append(carry_event) + elif event_type in [SB_EVENT_TYPE_DUEL, SB_EVENT_TYPE_50_50]: + duel_event_kwargs = _parse_duel( + raw_event=raw_event, event_type=event_type + ) + duel_event = self.event_factory.build_duel( + **duel_event_kwargs, + **generic_event_kwargs, + ) + new_events.append(duel_event) # lineup affecting events elif event_type == SB_EVENT_TYPE_SUBSTITUTION: @@ -849,6 +924,22 @@ def deserialize(self, inputs: StatsBombInputs) -> EventDataset: ) new_events.append(generic_event) + # 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] + ): + duel_event_kwargs = _parse_aerial_won_duel( + raw_event=raw_event, type_name=type_name + ) + duel_event = self.event_factory.build_duel( + **duel_event_kwargs, + **generic_event_kwargs, + ) + # add duel event as first event. + new_events.insert(0, 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 4693dced..9b3cdcb8 100644 --- a/kloppy/infra/serializers/event/wyscout/deserializer_v2.py +++ b/kloppy/infra/serializers/event/wyscout/deserializer_v2.py @@ -8,6 +8,9 @@ CardType, CounterAttackQualifier, Dimension, + DuelResult, + DuelQualifier, + DuelType, EventDataset, GoalkeeperAction, GoalkeeperActionQualifier, @@ -242,13 +245,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} @@ -393,9 +427,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 7e8538a1..e5cea060 100644 --- a/kloppy/infra/serializers/event/wyscout/deserializer_v3.py +++ b/kloppy/infra/serializers/event/wyscout/deserializer_v3.py @@ -3,25 +3,40 @@ from typing import Dict, List, Tuple, NamedTuple, IO from kloppy.domain import ( + BallOutEvent, BodyPart, BodyPartQualifier, + CardEvent, CardType, CounterAttackQualifier, + Dimension, + DuelType, + DuelQualifier, + DuelResult, EventDataset, + FoulCommittedEvent, + GenericEvent, + GoalkeeperAction, + GoalkeeperActionQualifier, Ground, Metadata, Orientation, + PassEvent, PassQualifier, PassResult, PassType, Period, + PitchDimensions, Player, Point, Provider, Qualifier, + RecoveryEvent, SetPieceQualifier, SetPieceType, + ShotEvent, ShotResult, + TakeOnEvent, TakeOnResult, Team, ) @@ -241,24 +256,66 @@ def _parse_set_piece(raw_event: Dict, next_event: Dict, team: Team) -> Dict: return result -def _parse_takeon(raw_event: Dict) -> Dict: +def _parse_duel(raw_event: Dict) -> Dict: qualifiers = _generic_qualifiers(raw_event) - result = None - if "offensive_duel" in raw_event["type"]["secondary"]: - if raw_event["groundDuel"]["keptPossession"]: - result = TakeOnResult.COMPLETE - else: - result = TakeOnResult.INCOMPLETE - elif "defensive_duel" in raw_event["type"]["secondary"]: - if raw_event["groundDuel"]["recoveredPossession"]: - result = TakeOnResult.COMPLETE - else: - result = TakeOnResult.INCOMPLETE - elif "aerial_duel" in raw_event["type"]["secondary"]: - if raw_event["aerialDuel"]["firstTouch"]: - result = TakeOnResult.COMPLETE - else: - result = TakeOnResult.INCOMPLETE + duel_qualifiers = [] + 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) + + 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} @@ -362,9 +419,9 @@ def deserialize(self, inputs: WyscoutInputs) -> EventDataset: **pass_event_args, **generic_event_args ) elif primary_event_type == "duel": - 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 primary_event_type == "clearance": clearance_event_args = _parse_clearance(raw_event) 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 a25e4146..e1f0dcb0 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"] @@ -77,7 +76,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"] speed = player_data["speed"] @@ -115,7 +113,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 @@ -205,7 +202,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/opta_f24.xml b/kloppy/tests/files/opta_f24.xml index 402d7f2b..4abc3e83 100644 --- a/kloppy/tests/files/opta_f24.xml +++ b/kloppy/tests/files/opta_f24.xml @@ -85,10 +85,8 @@ - - - - + + diff --git a/kloppy/tests/files/statsbomb_event.json b/kloppy/tests/files/statsbomb_event.json index df9cdbfb..6af07749 100644 --- a/kloppy/tests/files/statsbomb_event.json +++ b/kloppy/tests/files/statsbomb_event.json @@ -170128,6 +170128,126 @@ "name" : "Diving" } } +}, { + "id" : "d9152217-8772-454c-b461-9d1d66070859", + "index" : 1501, + "period" : 2, + "timestamp" : "00:48:01.770", + "minute" : 93, + "second" : 1, + "type" : { + "id" : 33, + "name" : "50/50" + }, + "possession" : 144, + "possession_team" : { + "id" : 217, + "name" : "Barcelona" + }, + "play_pattern" : { + "id" : 1, + "name" : "Regular Play" + }, + "team" : { + "id" : 217, + "name" : "Barcelona" + }, + "player" : { + "id" : 5503, + "name" : "Lionel Andrés Messi Cuccittini" + }, + "position" : { + "id" : 17, + "name" : "Right Wing" + }, + "location" : [ 47.4, 22.4 ], + "duration" : 0.0, + "under_pressure" : true, + "related_events" : [ "10bf8575-16df-43b2-b4b7-9854bb708944" ], + "50_50" : { + "outcome" : { + "id" : 3, + "name" : "Success To Team" + } + } +}, { + "id" : "d9152217-8772-454c-b461-9d1d66070129", + "index" : 1501, + "period" : 2, + "timestamp" : "00:48:01.770", + "minute" : 93, + "second" : 1, + "type" : { + "id" : 9, + "name" : "Clearance" + }, + "possession" : 144, + "possession_team" : { + "id" : 217, + "name" : "Barcelona" + }, + "play_pattern" : { + "id" : 1, + "name" : "Regular Play" + }, + "team" : { + "id" : 217, + "name" : "Barcelona" + }, + "player" : { + "id" : 5503, + "name" : "Lionel Andrés Messi Cuccittini" + }, + "position" : { + "id" : 17, + "name" : "Right Wing" + }, + "location" : [ 47.4, 22.4 ], + "duration" : 0.0, + "under_pressure" : true, + "related_events" : [ "54a0f549-fba3-4baa-8695-0bd92a2039bb", "8096dfc1-4842-41e1-8090-aa4c7117a499" ], + "clearance" : { + "aerial_won" : true + } +}, { + "id" : "d9152217-8772-454c-b461-9d1d42070129", + "index" : 1809, + "period" : 2, + "timestamp" : "00:48:01.770", + "minute" : 93, + "second" : 1, + "type" : { + "id" : 38, + "name" : "Miscontrol" + }, + "possession" : 144, + "possession_team" : { + "id" : 217, + "name" : "Barcelona" + }, + "play_pattern" : { + "id" : 1, + "name" : "Regular Play" + }, + "team" : { + "id" : 217, + "name" : "Barcelona" + }, + "player" : { + "id" : 5503, + "name" : "Lionel Andrés Messi Cuccittini" + }, + "position" : { + "id" : 17, + "name" : "Right Wing" + }, + "location" : [ 47.4, 22.4 ], + "duration" : 0.0, + "under_pressure" : true, + "related_events" : [ "09feb961-9f36-4c0e-a11d-9ab8eee9bf87" ], + "miscontrol" : { + "aerial_won" : true + } }, { "id" : "e1cc4d5e-ba55-4b6b-88cc-dae13311c1d9", "index" : 4001, @@ -170180,4 +170300,5 @@ }, "duration" : 0.0, "related_events" : [ "e1cc4d5e-ba55-4b6b-88cc-dae13311c1d9" ] -} ] \ No newline at end of file +} +] \ No newline at end of file diff --git a/kloppy/tests/files/wyscout_events_v3.json b/kloppy/tests/files/wyscout_events_v3.json index 7740c741..ac962c69 100644 --- a/kloppy/tests/files/wyscout_events_v3.json +++ b/kloppy/tests/files/wyscout_events_v3.json @@ -512,6 +512,217 @@ ] } }, + { + "id": 663291421, + "type": { + "primary": "duel", + "secondary": [ + "ground_duel", + "offensive_duel", + "loose_ball_duel" + ] + }, + "location": { + "x": 95, + "y": 7 + }, + "matchId": 2852835, + "matchPeriod": "1H", + "matchTimestamp": "00:00:08.295", + "minute": 0, + "opponentTeam": { + "formation": "3-4-3", + "id": 3185, + "name": "Torino" + }, + "shot": null, + "groundDuel": { + "opponent": { + "id": 636942, + "name": "N. Ngoy", + "position": "RCB3" + }, + "duelType": "offensive_duel", + "keptPossession": true, + "progressedWithBall": true, + "stoppedProgress": null, + "recoveredPossession": null, + "takeOn": false, + "side": null, + "relatedDuelId": 1331978561 + }, + "aerialDuel": null, + "player": { + "id": 20583, + "name": "Danilo", + "position": "RCB" + }, + "possession": { + "id": 663291837, + "duration": "1.261821", + "types": [ + "corner", + "set_piece_attack" + ], + "eventsNumber": 1, + "eventIndex": 0, + "startLocation": { + "x": 100, + "y": 0 + }, + "endLocation": { + "x": 98, + "y": 55 + }, + "team": { + "formation": "4-2-3-1", + "id": 3166, + "name": "Bologna" + }, + "attack": null + }, + "second": 8, + "team": { + "formation": "4-2-3-1", + "id": 3166, + "name": "Bologna" + }, + "videoTimestamp": "8.148438" + }, + { + "id": 663291840, + "type": { + "primary": "duel", + "secondary": [ + "aerial_duel", + "loss" + ] + }, + "location": { + "x": 96, + "y": 39 + }, + "matchId": 2852835, + "matchPeriod": "1H", + "matchTimestamp": "00:00:08.295", + "minute": 0, + "opponentTeam": { + "formation": "3-4-3", + "id": 3185, + "name": "Torino" + }, + "shot": null, + "groundDuel": null, + "aerialDuel": { + "opponent": { + "id": 0, + "name": null, + "position": null, + "height": null + }, + "firstTouch": true, + "height": 185, + "relatedDuelId": 1331979623 + }, + "player": { + "id": 20583, + "name": "Danilo", + "position": "RCB" + }, + "possession": { + "id": 663291837, + "duration": "1.261821", + "types": [ + "corner", + "set_piece_attack" + ], + "eventsNumber": 1, + "eventIndex": 0, + "startLocation": { + "x": 100, + "y": 0 + }, + "endLocation": { + "x": 98, + "y": 55 + }, + "team": { + "formation": "4-2-3-1", + "id": 3166, + "name": "Bologna" + }, + "attack": null + }, + "second": 8, + "team": { + "formation": "4-2-3-1", + "id": 3166, + "name": "Bologna" + }, + "videoTimestamp": "8.148438" + }, + { + "id": 663291840, + "type": { + "primary": "duel", + "secondary": [ + "loose_ball_duel", + "sliding_tackle" + ] + }, + "location": { + "x": 26, + "y": 32 + }, + "matchId": 2852835, + "matchPeriod": "1H", + "matchTimestamp": "00:00:08.295", + "minute": 0, + "opponentTeam": { + "formation": "3-4-3", + "id": 3185, + "name": "Torino" + }, + "shot": null, + "groundDuel": null, + "aerialDuel": null, + "player": { + "id": 20583, + "name": "Danilo", + "position": "RCB" + }, + "possession": { + "id": 663291837, + "duration": "1.261821", + "types": [ + "corner", + "set_piece_attack" + ], + "eventsNumber": 1, + "eventIndex": 0, + "startLocation": { + "x": 100, + "y": 0 + }, + "endLocation": { + "x": 98, + "y": 55 + }, + "team": { + "formation": "4-2-3-1", + "id": 3166, + "name": "Bologna" + }, + "attack": null + }, + "second": 8, + "team": { + "formation": "4-2-3-1", + "id": 3166, + "name": "Bologna" + }, + "videoTimestamp": "8.148438" + }, { "id": 663291842, "minute": 0, diff --git a/kloppy/tests/issues/issue_60/test_issue_60.py b/kloppy/tests/issues/issue_60/test_issue_60.py index 8841ca13..9af83093 100644 --- a/kloppy/tests/issues/issue_60/test_issue_60.py +++ b/kloppy/tests/issues/issue_60/test_issue_60.py @@ -18,5 +18,5 @@ def test_deleted_event_opta(self): # OPTA F24 file: Pass -> Deleted Event -> Tackle assert event_dataset.events[14].event_name == "pass" assert ( - event_dataset.events[15].event_name == "tackle" + event_dataset.events[15].event_name == "duel" ) # Deleted Event is filter out diff --git a/kloppy/tests/test_helpers.py b/kloppy/tests/test_helpers.py index f678e8f2..d320b6fa 100644 --- a/kloppy/tests/test_helpers.py +++ b/kloppy/tests/test_helpers.py @@ -374,7 +374,7 @@ def test_event_dataset_to_polars(self, base_dir): import polars as pl c = df.select(pl.col("event_id").count())[0, 0] - assert c == 4023 + assert c == 4039 def test_tracking_dataset_to_polars(self): """ diff --git a/kloppy/tests/test_opta.py b/kloppy/tests/test_opta.py index 79024bcd..51d4a36f 100644 --- a/kloppy/tests/test_opta.py +++ b/kloppy/tests/test_opta.py @@ -13,6 +13,8 @@ DatasetType, CardType, FormationType, + DuelQualifier, + DuelType, CounterAttackQualifier, ) @@ -117,6 +119,16 @@ def test_correct_deserialization(self, f7_data: str, f24_data: str): CounterAttackQualifier(value=True) in dataset.events[17].qualifiers ) # 2318695229 + # 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 + ) + 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 b6bf052c..93b85355 100644 --- a/kloppy/tests/test_statsbomb.py +++ b/kloppy/tests/test_statsbomb.py @@ -8,6 +8,8 @@ BodyPart, BodyPartQualifier, DatasetType, + DuelQualifier, + DuelType, Orientation, Period, Point, @@ -51,7 +53,7 @@ def test_correct_deserialization( assert dataset.metadata.provider == Provider.STATSBOMB assert dataset.dataset_type == DatasetType.EVENT - assert len(dataset.events) == 4023 + assert len(dataset.events) == 4039 assert len(dataset.metadata.periods) == 2 assert ( dataset.metadata.orientation == Orientation.ACTION_EXECUTING_TEAM @@ -94,7 +96,7 @@ def test_correct_deserialization( assert dataset.events[10].coordinates == Point(34.5, 20.5) assert ( - dataset.events[792].get_qualifier_value(BodyPartQualifier) + dataset.events[794].get_qualifier_value(BodyPartQualifier) == BodyPart.HEAD ) @@ -108,48 +110,56 @@ def test_correct_deserialization( ) assert ( - dataset.events[1433].get_qualifier_value(PassQualifier) + dataset.events[1438].get_qualifier_value(PassQualifier) == PassType.CROSS ) assert ( - dataset.events[1552].get_qualifier_value(PassQualifier) + dataset.events[1557].get_qualifier_value(PassQualifier) == PassType.THROUGH_BALL ) assert ( - dataset.events[443].get_qualifier_value(PassQualifier) + dataset.events[444].get_qualifier_value(PassQualifier) == PassType.SWITCH_OF_PLAY ) assert ( - dataset.events[3438].get_qualifier_value(PassQualifier) + dataset.events[101].get_qualifier_value(PassQualifier) == PassType.LONG_BALL ) assert ( - dataset.events[2266].get_qualifier_value(PassQualifier) + dataset.events[17].get_qualifier_value(PassQualifier) == PassType.HIGH_PASS ) assert ( - dataset.events[653].get_qualifier_value(PassQualifier) + dataset.events[654].get_qualifier_value(PassQualifier) == PassType.HEAD_PASS ) assert ( - dataset.events[3134].get_qualifier_value(PassQualifier) + dataset.events[3145].get_qualifier_value(PassQualifier) == PassType.HAND_PASS ) assert ( - dataset.events[3611].get_qualifier_value(PassQualifier) + dataset.events[3622].get_qualifier_value(PassQualifier) == PassType.ASSIST ) - assert dataset.events[3392].get_qualifier_value(PassQualifier) is None + assert dataset.events[3400].get_qualifier_value(PassQualifier) is None - assert dataset.events[271].event_type == EventType.CLEARANCE + assert ( + dataset.events[194].get_qualifier_values(DuelQualifier)[1].value + == DuelType.AERIAL + ) + assert ( + dataset.events[4032].get_qualifier_values(DuelQualifier)[1].value + == DuelType.GROUND + ) + assert dataset.events[272].event_type == EventType.CLEARANCE 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 da706067..493f8ada 100644 --- a/kloppy/tests/test_wyscout.py +++ b/kloppy/tests/test_wyscout.py @@ -1,7 +1,14 @@ from pathlib import Path import pytest -from kloppy.domain import Point, SetPieceType, SetPieceQualifier, EventType +from kloppy.domain import ( + Point, + SetPieceType, + SetPieceQualifier, + DuelQualifier, + DuelType, + EventType, +) from kloppy import wyscout @@ -23,13 +30,26 @@ def test_correct_v3_deserialization(self, event_v3_data: Path): coordinates="wyscout", data_version="V3", ) - assert dataset.events[2].coordinates == Point(36.0, 78.0) + df = dataset.to_df() + assert dataset.records[2].coordinates == Point(36.0, 78.0) assert ( dataset.events[4].get_qualifier_value(SetPieceQualifier) == SetPieceType.CORNER_KICK ) assert dataset.events[5].event_type == EventType.FOUL_COMMITTED - assert dataset.events[6].event_type == EventType.CLEARANCE + assert ( + dataset.events[6].get_qualifier_value(DuelQualifier) + == DuelType.GROUND + ) + assert ( + dataset.events[7].get_qualifier_values(DuelQualifier)[1].value + == DuelType.AERIAL + ) + assert ( + dataset.events[8].get_qualifier_values(DuelQualifier)[2].value + == DuelType.SLIDING_TACKLE + ) + assert dataset.events[9].event_type == EventType.CLEARANCE def test_correct_normalized_v3_deserialization(self, event_v3_data: Path): dataset = wyscout.load(event_data=event_v3_data, data_version="V3") @@ -44,6 +64,19 @@ def test_correct_v2_deserialization(self, event_v2_data: Path): assert dataset.records[2].coordinates == Point(29.0, 6.0) assert dataset.events[136].event_type == EventType.CLEARANCE + 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