diff --git a/kloppy/infra/serializers/event/statsbomb/deserializer.py b/kloppy/infra/serializers/event/statsbomb/deserializer.py index 0e90430d..ee78590d 100644 --- a/kloppy/infra/serializers/event/statsbomb/deserializer.py +++ b/kloppy/infra/serializers/event/statsbomb/deserializer.py @@ -217,10 +217,24 @@ def _parse_coordinates( # +-----+------+ cell_side = 0.1 if fidelity_version == 2 else 1.0 cell_relative_center = cell_side / 2 - return Point( - x=coordinates[0] - cell_relative_center, - y=coordinates[1] - cell_relative_center, - ) + if len(coordinates) == 2: + return Point( + x=coordinates[0] - cell_relative_center, + y=coordinates[1] - cell_relative_center, + ) + elif len(coordinates) == 3: + # A coordinate in the goal frame, only used for the end location of + # Shot events. The y-coordinates and z-coordinates are always detailed + # to a tenth of a yard. + return Point3D( + x=coordinates[0] - cell_relative_center, + y=coordinates[1] - 0.05, + z=coordinates[2] - 0.05, + ) + else: + raise DeserializationError( + f"Unknown coordinates format: {coordinates}" + ) def _get_body_part_qualifiers( @@ -385,7 +399,7 @@ def _parse_pass(pass_dict: Dict, team: Team, fidelity_version: int) -> Dict: } -def _parse_shot(shot_dict: Dict) -> Dict: +def _parse_shot(shot_dict: Dict, fidelity_version: int) -> Dict: outcome_id = shot_dict["outcome"]["id"] if outcome_id == SB_SHOT_OUTCOME_OFF_TARGET: result = ShotResult.OFF_TARGET @@ -412,7 +426,14 @@ def _parse_shot(shot_dict: Dict) -> Dict: body_part_qualifiers = _get_body_part_qualifiers(shot_dict) qualifiers.extend(body_part_qualifiers) - return {"result": result, "qualifiers": qualifiers} + return { + "result": result, + "qualifiers": qualifiers, + "result_coordinates": _parse_coordinates( + shot_dict["end_location"], + fidelity_version, + ), + } def _parse_freeze_frame( @@ -903,6 +924,7 @@ def deserialize(self, inputs: StatsBombInputs) -> EventDataset: elif event_type == SB_EVENT_TYPE_SHOT: shot_event_kwargs = _parse_shot( shot_dict=raw_event["shot"], + fidelity_version=shot_fidelity_version, ) shot_event = self.event_factory.build_shot( **shot_event_kwargs, diff --git a/kloppy/tests/test_statsbomb.py b/kloppy/tests/test_statsbomb.py index 66949d33..f5b232a1 100644 --- a/kloppy/tests/test_statsbomb.py +++ b/kloppy/tests/test_statsbomb.py @@ -13,8 +13,11 @@ Orientation, Period, Point, + Point3D, Provider, FormationType, + SetPieceQualifier, + SetPieceType, Position, ShotResult, ) @@ -255,6 +258,34 @@ def test_foul_committed(self, lineup_data: Path, event_data: Path): assert len(dataset.events) == 23 + def test_shot(self, lineup_data: Path, event_data: Path): + """ + Test shot events + """ + dataset = statsbomb.load( + lineup_data=lineup_data, + event_data=event_data, + event_types=["shot"], + coordinates="statsbomb", + ) + + assert len(dataset.events) == 29 + + shot = dataset.get_event_by_id("39f231e5-0072-461c-beb0-a9bedb420f83") + # A shot event should have a result + assert shot.result == ShotResult.POST + # A shot event should have end coordinates + assert shot.result_coordinates == Point3D(119.95, 42.95, 2.75) + # A shot event should have a body part + assert ( + shot.get_qualifier_value(BodyPartQualifier) == BodyPart.LEFT_FOOT + ) + # A shot event can have set piece qualifiers + assert ( + shot.get_qualifier_value(SetPieceQualifier) + == SetPieceType.FREE_KICK + ) + def test_own_goal(self, lineup_data: Path, event_data: Path): """ Test own goal events.