From 2d95aa2e07a3b801e0345d51eb9afb5d3e01b449 Mon Sep 17 00:00:00 2001 From: BenjaminLarrousse Date: Wed, 20 Jan 2021 18:43:57 +0100 Subject: [PATCH 1/5] Add body_part for pass events and shot events in Statsbomb's serializer. --- kloppy/domain/models/event.py | 29 +++++++++- .../serializers/event/statsbomb/serializer.py | 57 ++++++++++++++++++- kloppy/tests/test_statsbomb.py | 4 ++ 3 files changed, 86 insertions(+), 4 deletions(-) diff --git a/kloppy/domain/models/event.py b/kloppy/domain/models/event.py index 413c7ae7..df2bff70 100644 --- a/kloppy/domain/models/event.py +++ b/kloppy/domain/models/event.py @@ -263,15 +263,34 @@ class BodyPart(Enum): BodyPart Attributes: - RIGHT_FOOT (BodyPart): - LEFT_FOOT (BodyPart): - HEAD (BodyPart): + RIGHT_FOOT (BodyPart): Pass or Shot with right foot, save with right foot (for goalkeepers). + LEFT_FOOT (BodyPart): Pass or Shot with leftt foot, save with left foot (for goalkeepers). + HEAD (BodyPart): Pass or Shot with head, save with head (for goalkeepers). + BOTH_HANDS (BodyPart): Goalkeeper only. Save with both hands. + CHEST (BodyPart): Goalkeeper only. Save with chest. + LEFT_HAND (BodyPart): Goalkeeper only. Save with left hand. + RIGHT_HAND (BodyPart): Goalkeeper only. Save with right hand. + DROP_KICK (BodyPart): Pass is a keeper drop kick. + KEEPER_ARM (BodyPart): Pass thrown from keepers hands. + OTHER (BodyPart): Other body part (chest, back, etc.), for Pass and Shot. + NO_TOUCH (BodyPart): Pass only. A player deliberately let the pass go past him + instead of receiving it to deliver to a teammate behind him. + (Also known as a "dummy"). """ RIGHT_FOOT = "RIGHT_FOOT" LEFT_FOOT = "LEFT_FOOT" HEAD = "HEAD" + BOTH_HANDS = "BOTH_HANDS" + CHEST = "CHEST" + LEFT_HAND = "LEFT_HAND" + RIGHT_HAND = "RIGHT_HAND" + DROP_KICK = "DROP_KICK" + KEEPER_ARM = "KEEPER_ARM" + OTHER = "OTHER" + NO_TOUCH = "NO_TOUCH" + @dataclass class BodyPartQualifier(EnumQualifier): @@ -383,6 +402,8 @@ class ShotEvent(Event): result (ShotResult): See [`ShotResult`][kloppy.domain.models.event.ShotResult] """ + body_part: BodyPartQualifier + result: ShotResult result_coordinates: Point = None @@ -411,6 +432,8 @@ class PassEvent(Event): result: PassResult + body_part: BodyPartQualifier + event_type: EventType = EventType.PASS event_name: str = "pass" diff --git a/kloppy/infra/serializers/event/statsbomb/serializer.py b/kloppy/infra/serializers/event/statsbomb/serializer.py index ae39017e..6b7ef1c2 100644 --- a/kloppy/infra/serializers/event/statsbomb/serializer.py +++ b/kloppy/infra/serializers/event/statsbomb/serializer.py @@ -38,6 +38,7 @@ FoulCommittedEvent, BallOutEvent, Event, + BodyPart, ) from kloppy.infra.serializers.event import EventDataSerializer from kloppy.utils import Readable, performance_logging @@ -83,6 +84,18 @@ OUT_EVENT_RESULTS = [PassResult.OUT, TakeOnResult.OUT] +SB_BODYPART_BOTH_HANDS = 35 +SB_BODYPART_CHEST = 36 +SB_BODYPART_HEAD = 37 +SB_BODYPART_LEFT_FOOT = 38 +SB_BODYPART_LEFT_HAND = 39 +SB_BODYPART_RIGHT_FOOT = 40 +SB_BODYPART_RIGHT_HAND = 41 +SB_BODYPART_DROP_KICK = 68 +SB_BODYPART_KEEPER_ARM = 69 +SB_BODYPART_OTHER = 70 +SB_BODYPART_NO_TOUCH = 106 + def parse_str_ts(timestamp: str) -> float: h, m, s = timestamp.split(":") @@ -107,6 +120,39 @@ def _parse_coordinates( ) +def _parse_bodypart(event_dict: Dict) -> BodyPart: + if "body_part" in event_dict: + bodypart_id = event_dict["body_part"]["id"] + if bodypart_id == SB_BODYPART_BOTH_HANDS: + body_part = BodyPart.BOTH_HANDS + elif bodypart_id == SB_BODYPART_CHEST: + body_part = BodyPart.CHEST + elif bodypart_id == SB_BODYPART_HEAD: + body_part = BodyPart.HEAD + elif bodypart_id == SB_BODYPART_LEFT_FOOT: + body_part = BodyPart.LEFT_FOOT + elif bodypart_id == SB_BODYPART_LEFT_HAND: + body_part = BodyPart.LEFT_HAND + elif bodypart_id == SB_BODYPART_RIGHT_FOOT: + body_part = BodyPart.RIGHT_FOOT + elif bodypart_id == SB_BODYPART_RIGHT_HAND: + body_part = BodyPart.RIGHT_HAND + elif bodypart_id == SB_BODYPART_DROP_KICK: + body_part = BodyPart.DROP_KICK + elif bodypart_id == SB_BODYPART_KEEPER_ARM: + body_part = BodyPart.KEEPER_ARM + elif bodypart_id == SB_BODYPART_OTHER: + body_part = BodyPart.OTHER + elif bodypart_id == SB_BODYPART_NO_TOUCH: + body_part = BodyPart.NO_TOUCH + else: + raise Exception(f"Unknown body part: {body_part_id}") + else: + body_part = None + + return body_part + + def _parse_pass(pass_dict: Dict, team: Team, fidelity_version: int) -> Dict: if "outcome" in pass_dict: outcome_id = pass_dict["outcome"]["id"] @@ -134,11 +180,14 @@ def _parse_pass(pass_dict: Dict, team: Team, fidelity_version: int) -> Dict: qualifiers = _get_event_qualifiers(pass_dict) + body_part = _parse_bodypart(pass_dict) + return dict( result=result, receiver_coordinates=receiver_coordinates, receiver_player=receiver_player, qualifiers=qualifiers, + body_part=body_part, ) @@ -182,7 +231,13 @@ def _parse_shot(shot_dict: Dict) -> Dict: qualifiers = _get_event_qualifiers(shot_dict) - return dict(result=result, qualifiers=qualifiers) + body_part = _parse_bodypart(shot_dict) + + return dict( + result=result, + qualifiers=qualifiers, + body_part=body_part, + ) def _parse_carry(carry_dict: Dict, fidelity_version: int) -> Dict: diff --git a/kloppy/tests/test_statsbomb.py b/kloppy/tests/test_statsbomb.py index 5c5bda31..6a1f730b 100644 --- a/kloppy/tests/test_statsbomb.py +++ b/kloppy/tests/test_statsbomb.py @@ -68,6 +68,10 @@ def test_correct_deserialization(self): attacking_direction=AttackingDirection.NOT_SET, ) + assert dataset.events[791].body_part.value == "HEAD" + assert dataset.events[2231].body_part.value == "RIGHT_FOOT" + assert dataset.events[195].body_part is None + def test_substitution(self): """ Test substitution events From 71404ed00173653a1f8e33fcb8a98b02a89480ee Mon Sep 17 00:00:00 2001 From: BenjaminLarrousse Date: Wed, 20 Jan 2021 19:08:49 +0100 Subject: [PATCH 2/5] Add a default value to body_part in PassEvent class and ShotEvent class. --- kloppy/domain/models/event.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/kloppy/domain/models/event.py b/kloppy/domain/models/event.py index df2bff70..120b1ac8 100644 --- a/kloppy/domain/models/event.py +++ b/kloppy/domain/models/event.py @@ -400,16 +400,17 @@ class ShotEvent(Event): event_name (str): `"shot"`, result_coordinates (Point): See [`Point`][kloppy.domain.models.pitch.Point] result (ShotResult): See [`ShotResult`][kloppy.domain.models.event.ShotResult] + body_part (BodyPartQualifier): body part attributes. """ - body_part: BodyPartQualifier - result: ShotResult result_coordinates: Point = None event_type: EventType = EventType.SHOT event_name: str = "shot" + body_part: BodyPartQualifier = None + @dataclass @docstring_inherit_attributes(Event) @@ -424,6 +425,7 @@ class PassEvent(Event): receiver_coordinates (Point): See [`Point`][kloppy.domain.models.pitch.Point] receiver_player (Player): See [`Player`][kloppy.domain.models.common.Player] result (PassResult): See [`PassResult`][kloppy.domain.models.event.PassResult] + body_part (BodyPartQualifier): body part attributes. """ receive_timestamp: float @@ -432,11 +434,11 @@ class PassEvent(Event): result: PassResult - body_part: BodyPartQualifier - event_type: EventType = EventType.PASS event_name: str = "pass" + body_part: BodyPartQualifier = None + @dataclass @docstring_inherit_attributes(Event) From 8bb089a032f049a8656b71cc88faf3783b0bbcfe Mon Sep 17 00:00:00 2001 From: BenjaminLarrousse Date: Fri, 22 Jan 2021 16:13:53 +0100 Subject: [PATCH 3/5] Move body_part attribute into qualifier via the _get_event_qualifiers() function. --- .../serializers/event/statsbomb/serializer.py | 61 +++++++++---------- kloppy/tests/test_statsbomb.py | 11 +++- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/kloppy/infra/serializers/event/statsbomb/serializer.py b/kloppy/infra/serializers/event/statsbomb/serializer.py index 6b7ef1c2..a1163c4d 100644 --- a/kloppy/infra/serializers/event/statsbomb/serializer.py +++ b/kloppy/infra/serializers/event/statsbomb/serializer.py @@ -39,6 +39,7 @@ BallOutEvent, Event, BodyPart, + BodyPartQualifier, ) from kloppy.infra.serializers.event import EventDataSerializer from kloppy.utils import Readable, performance_logging @@ -121,34 +122,31 @@ def _parse_coordinates( def _parse_bodypart(event_dict: Dict) -> BodyPart: - if "body_part" in event_dict: - bodypart_id = event_dict["body_part"]["id"] - if bodypart_id == SB_BODYPART_BOTH_HANDS: - body_part = BodyPart.BOTH_HANDS - elif bodypart_id == SB_BODYPART_CHEST: - body_part = BodyPart.CHEST - elif bodypart_id == SB_BODYPART_HEAD: - body_part = BodyPart.HEAD - elif bodypart_id == SB_BODYPART_LEFT_FOOT: - body_part = BodyPart.LEFT_FOOT - elif bodypart_id == SB_BODYPART_LEFT_HAND: - body_part = BodyPart.LEFT_HAND - elif bodypart_id == SB_BODYPART_RIGHT_FOOT: - body_part = BodyPart.RIGHT_FOOT - elif bodypart_id == SB_BODYPART_RIGHT_HAND: - body_part = BodyPart.RIGHT_HAND - elif bodypart_id == SB_BODYPART_DROP_KICK: - body_part = BodyPart.DROP_KICK - elif bodypart_id == SB_BODYPART_KEEPER_ARM: - body_part = BodyPart.KEEPER_ARM - elif bodypart_id == SB_BODYPART_OTHER: - body_part = BodyPart.OTHER - elif bodypart_id == SB_BODYPART_NO_TOUCH: - body_part = BodyPart.NO_TOUCH - else: - raise Exception(f"Unknown body part: {body_part_id}") + bodypart_id = event_dict["body_part"]["id"] + if bodypart_id == SB_BODYPART_BOTH_HANDS: + body_part = BodyPart.BOTH_HANDS + elif bodypart_id == SB_BODYPART_CHEST: + body_part = BodyPart.CHEST + elif bodypart_id == SB_BODYPART_HEAD: + body_part = BodyPart.HEAD + elif bodypart_id == SB_BODYPART_LEFT_FOOT: + body_part = BodyPart.LEFT_FOOT + elif bodypart_id == SB_BODYPART_LEFT_HAND: + body_part = BodyPart.LEFT_HAND + elif bodypart_id == SB_BODYPART_RIGHT_FOOT: + body_part = BodyPart.RIGHT_FOOT + elif bodypart_id == SB_BODYPART_RIGHT_HAND: + body_part = BodyPart.RIGHT_HAND + elif bodypart_id == SB_BODYPART_DROP_KICK: + body_part = BodyPart.DROP_KICK + elif bodypart_id == SB_BODYPART_KEEPER_ARM: + body_part = BodyPart.KEEPER_ARM + elif bodypart_id == SB_BODYPART_OTHER: + body_part = BodyPart.OTHER + elif bodypart_id == SB_BODYPART_NO_TOUCH: + body_part = BodyPart.NO_TOUCH else: - body_part = None + raise Exception(f"Unknown body part: {body_part_id}") return body_part @@ -180,14 +178,11 @@ def _parse_pass(pass_dict: Dict, team: Team, fidelity_version: int) -> Dict: qualifiers = _get_event_qualifiers(pass_dict) - body_part = _parse_bodypart(pass_dict) - return dict( result=result, receiver_coordinates=receiver_coordinates, receiver_player=receiver_player, qualifiers=qualifiers, - body_part=body_part, ) @@ -209,6 +204,9 @@ def _get_event_qualifiers(qualifiers_dict: Dict) -> List[Qualifier]: elif qualifiers_dict["type"]["id"] == SB_EVENT_TYPE_GOAL_KICK: qualifiers.append(SetPieceQualifier(value=SetPieceType.GOAL_KICK)) + if "body_part" in qualifiers_dict: + qualifiers.append(BodyPartQualifier(_parse_bodypart(qualifiers_dict))) + return qualifiers @@ -231,12 +229,9 @@ def _parse_shot(shot_dict: Dict) -> Dict: qualifiers = _get_event_qualifiers(shot_dict) - body_part = _parse_bodypart(shot_dict) - return dict( result=result, qualifiers=qualifiers, - body_part=body_part, ) diff --git a/kloppy/tests/test_statsbomb.py b/kloppy/tests/test_statsbomb.py index 6a1f730b..598f90e2 100644 --- a/kloppy/tests/test_statsbomb.py +++ b/kloppy/tests/test_statsbomb.py @@ -7,6 +7,7 @@ Orientation, Provider, EventType, + BodyPartQualifier, ) from kloppy.domain.models.common import DatasetType @@ -68,9 +69,13 @@ def test_correct_deserialization(self): attacking_direction=AttackingDirection.NOT_SET, ) - assert dataset.events[791].body_part.value == "HEAD" - assert dataset.events[2231].body_part.value == "RIGHT_FOOT" - assert dataset.events[195].body_part is None + for qualifier in dataset.events[791].qualifiers: + if qualifier == BodyPartQualifier: + assert qualifier.value == "HEAD" + + for qualifier in dataset.events[2231].qualifiers: + if qualifier == BodyPartQualifier: + assert qualifier.value == "RIGHT_FOOT" def test_substitution(self): """ From 4a54d309295f87484ecdef431eb7420951e0c64b Mon Sep 17 00:00:00 2001 From: BenjaminLarrousse Date: Fri, 22 Jan 2021 19:03:12 +0100 Subject: [PATCH 4/5] Remove attributes in ShotEvent and PassEvent classes. Create a better test. --- kloppy/domain/models/event.py | 6 ------ kloppy/tests/test_statsbomb.py | 18 ++++++++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/kloppy/domain/models/event.py b/kloppy/domain/models/event.py index 120b1ac8..b2fa18b3 100644 --- a/kloppy/domain/models/event.py +++ b/kloppy/domain/models/event.py @@ -400,7 +400,6 @@ class ShotEvent(Event): event_name (str): `"shot"`, result_coordinates (Point): See [`Point`][kloppy.domain.models.pitch.Point] result (ShotResult): See [`ShotResult`][kloppy.domain.models.event.ShotResult] - body_part (BodyPartQualifier): body part attributes. """ result: ShotResult @@ -409,8 +408,6 @@ class ShotEvent(Event): event_type: EventType = EventType.SHOT event_name: str = "shot" - body_part: BodyPartQualifier = None - @dataclass @docstring_inherit_attributes(Event) @@ -425,7 +422,6 @@ class PassEvent(Event): receiver_coordinates (Point): See [`Point`][kloppy.domain.models.pitch.Point] receiver_player (Player): See [`Player`][kloppy.domain.models.common.Player] result (PassResult): See [`PassResult`][kloppy.domain.models.event.PassResult] - body_part (BodyPartQualifier): body part attributes. """ receive_timestamp: float @@ -437,8 +433,6 @@ class PassEvent(Event): event_type: EventType = EventType.PASS event_name: str = "pass" - body_part: BodyPartQualifier = None - @dataclass @docstring_inherit_attributes(Event) diff --git a/kloppy/tests/test_statsbomb.py b/kloppy/tests/test_statsbomb.py index 598f90e2..abec65d6 100644 --- a/kloppy/tests/test_statsbomb.py +++ b/kloppy/tests/test_statsbomb.py @@ -69,13 +69,19 @@ def test_correct_deserialization(self): attacking_direction=AttackingDirection.NOT_SET, ) - for qualifier in dataset.events[791].qualifiers: - if qualifier == BodyPartQualifier: - assert qualifier.value == "HEAD" + assert ( + dataset.events[791].get_qualifier_value(BodyPartQualifier).value + == "HEAD" + ) - for qualifier in dataset.events[2231].qualifiers: - if qualifier == BodyPartQualifier: - assert qualifier.value == "RIGHT_FOOT" + assert ( + dataset.events[2231].get_qualifier_value(BodyPartQualifier).value + == "RIGHT_FOOT" + ) + + assert ( + dataset.events[195].get_qualifier_value(BodyPartQualifier) is None + ) def test_substitution(self): """ From a8332571e22f4e7ace0d2571c56254803dbf9d57 Mon Sep 17 00:00:00 2001 From: BenjaminLarrousse Date: Tue, 26 Jan 2021 12:22:33 +0100 Subject: [PATCH 5/5] Small modification of tests. --- kloppy/tests/test_statsbomb.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/kloppy/tests/test_statsbomb.py b/kloppy/tests/test_statsbomb.py index abec65d6..36b1ddc5 100644 --- a/kloppy/tests/test_statsbomb.py +++ b/kloppy/tests/test_statsbomb.py @@ -8,6 +8,7 @@ Provider, EventType, BodyPartQualifier, + BodyPart, ) from kloppy.domain.models.common import DatasetType @@ -70,13 +71,13 @@ def test_correct_deserialization(self): ) assert ( - dataset.events[791].get_qualifier_value(BodyPartQualifier).value - == "HEAD" + dataset.events[791].get_qualifier_value(BodyPartQualifier) + == BodyPart.HEAD ) assert ( - dataset.events[2231].get_qualifier_value(BodyPartQualifier).value - == "RIGHT_FOOT" + dataset.events[2231].get_qualifier_value(BodyPartQualifier) + == BodyPart.RIGHT_FOOT ) assert (